From 8e7141175afae04caf5bbf532d0909f309cca752 Mon Sep 17 00:00:00 2001 From: O K Date: Tue, 25 Nov 2025 11:00:26 +0200 Subject: [PATCH] improve photos --- .gitignore | 4 +- addlivephoto.php | 63 +- .../admin/AdminAddLivePhotoController.php | 296 ++++----- views/js/admin.js | 465 ++++++------- views/templates/admin/uploader.tpl | 615 +++++++++++++++--- .../hook/displayProductPriceBlock.tpl | 280 +++----- 6 files changed, 1004 insertions(+), 719 deletions(-) diff --git a/.gitignore b/.gitignore index 4b537fe..0e80661 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/photo \ No newline at end of file +/photo +.llmdump +llmdumper.php \ No newline at end of file diff --git a/addlivephoto.php b/addlivephoto.php index a65e7e0..5c575d1 100644 --- a/addlivephoto.php +++ b/addlivephoto.php @@ -84,16 +84,14 @@ class AddLivePhoto extends Module $this->uninstallAdminTab(); } - /** - * Create the database table for storing image information. - * @return bool - */ - protected function installDb() + protected function installDb() { + // Added image_type column $sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . self::TABLE_NAME . '` ( `id_add_live_photo` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT, `id_product` INT(11) UNSIGNED NOT NULL, `image_name` VARCHAR(255) NOT NULL, + `image_type` ENUM("expiry", "packaging") NOT NULL DEFAULT "expiry", `date_add` DATETIME NOT NULL, PRIMARY KEY (`id_add_live_photo`), INDEX `id_product_idx` (`id_product`) @@ -177,48 +175,51 @@ class AddLivePhoto extends Module } $id_product = (int) Tools::getValue('id_product'); - if (!$id_product) { - return; - } + if (!$id_product) return; - // Fetch images from the last 4 months - $sql = new DbQuery(); - $sql->select('`image_name`'); - $sql->from(self::TABLE_NAME); - $sql->where('`id_product` = ' . $id_product); - $sql->where('`date_add` >= DATE_SUB(NOW(), INTERVAL 4 MONTH)'); - $sql->orderBy('`date_add` DESC'); + // Complex Logic: + // 1. Get 'packaging' photos (Always show, limit to newest 3) + // 2. Get 'expiry' photos (Show only if newer than 3 months) + + $sql = "SELECT * FROM `" . _DB_PREFIX_ . self::TABLE_NAME . "` + WHERE `id_product` = " . $id_product . " + AND ( + (`image_type` = 'packaging') + OR + (`image_type` = 'expiry' AND `date_add` >= DATE_SUB(NOW(), INTERVAL 3 MONTH)) + ) + ORDER BY `date_add` DESC"; $results = Db::getInstance()->executeS($sql); - if (!$results) { - return; - } + if (!$results) return; $live_photos = []; foreach ($results as $row) { $image_uri = $this->getProductImageUri($id_product, $row['image_name']); if ($image_uri) { + + // Customize text based on type + $is_expiry = ($row['image_type'] === 'expiry'); + $date_taken = date('Y-m-d', strtotime($row['date_add'])); + + $alt_text = $is_expiry + ? sprintf($this->trans('Expiry date photo for %s, taken on %s',[], 'Modules.Addlivephoto.Shop'), $this->context->smarty->tpl_vars['product']->value['name'], $date_taken) + : sprintf($this->trans('Real packaging photo for %s',[], 'Modules.Addlivephoto.Shop'), $this->context->smarty->tpl_vars['product']->value['name']); + $live_photos[] = [ 'url' => $image_uri, - // This alt text is crucial for SEO - 'alt' => sprintf( - $this->trans('Freshness photo for %s, taken on %s',[], 'Modules.Addlivephoto.Shop'), - $this->context->smarty->tpl_vars['product']->value['name'], - date('Y-m-d') // You can store the date_add and format it here - ), - 'title' => $this->trans('Click to see the expiry date photo',[], 'Modules.Addlivephoto.Shop'), + 'type' => $row['image_type'], // 'expiry' or 'packaging' + 'date' => $row['date_add'], + 'alt' => $alt_text, ]; } } - if (empty($live_photos)) { - return; - } + if (empty($live_photos)) return; $this->context->smarty->assign([ 'live_photos' => $live_photos, - 'module_name' => $this->name, ]); return $this->display(__FILE__, 'views/templates/hook/displayProductPriceBlock.tpl'); @@ -231,8 +232,8 @@ class AddLivePhoto extends Module { // We only want to load these assets on our specific controller page if (Tools::getValue('controller') == 'AdminAddLivePhoto') { - $this->context->controller->addJS($this->_path . 'views/js/admin.js'); - $this->context->controller->addCSS($this->_path . 'views/css/admin.css'); + // $this->context->controller->addJS($this->_path . 'views/js/admin.js'); + // $this->context->controller->addCSS($this->_path . 'views/css/admin.css'); } } diff --git a/controllers/admin/AdminAddLivePhotoController.php b/controllers/admin/AdminAddLivePhotoController.php index 6de9ff7..50fb08e 100644 --- a/controllers/admin/AdminAddLivePhotoController.php +++ b/controllers/admin/AdminAddLivePhotoController.php @@ -1,30 +1,6 @@ - * @copyright 2007-2023 PrestaShop SA - * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) - * International Registered Trademark & Property of PrestaShop SA - * - * @property \AddLivePhoto $module + * Admin Controller for AddLivePhoto Module */ class AdminAddLivePhotoController extends ModuleAdminController @@ -32,205 +8,211 @@ class AdminAddLivePhotoController extends ModuleAdminController public function __construct() { $this->bootstrap = true; - // The table is not for a list view, but it's good practice to set it. - $this->table = 'product'; + $this->display = 'view'; // Force custom view parent::__construct(); } - /** - * This is the entry point for the controller page. - * It sets up the main template. - */ public function initContent() { - parent::initContent(); - - // Pass the ajax URL to the template - $ajax_url = $this->context->link->getAdminLink( - 'AdminAddLivePhoto', - true, // Keep the token - [], // No route params - ['ajax' => 1] // Add ajax=1 to the query string - ); + // Не викликаємо parent::initContent(), бо нам не потрібен стандартний список + // Але нам потрібен header і footer адмінки $this->context->smarty->assign([ - 'ajax_url' => $ajax_url, + 'content' => $this->renderView(), // Це вставить наш tpl ]); - - // We use a custom template for our camera interface. - $this->setTemplate('uploader.tpl'); + + // Викликаємо батьківський метод для відображення структури адмінки + parent::initContent(); + } + + public function renderView() + { + $ajax_url = $this->context->link->getAdminLink( + 'AdminAddLivePhoto', + true, + [], + ['ajax' => 1] + ); + + $this->context->smarty->assign([ + 'ajax_url' => $ajax_url, + 'module_dir' => _MODULE_DIR_ . $this->module->name . '/', + ]); + + return $this->context->smarty->fetch($this->module->getLocalPath() . 'views/templates/admin/uploader.tpl'); } - /** - * This method is automatically called by PrestaShop when an AJAX request is made to this controller. - * We use a 'action' parameter to decide what to do. - */ public function ajaxProcess() { $action = Tools::getValue('action'); - switch ($action) { - case 'searchProduct': - $this->ajaxProcessSearchProduct(); - break; - case 'uploadImage': - $this->ajaxProcessUploadImage(); - break; - case 'deleteImage': - $this->ajaxProcessDeleteImage(); - break; + + try { + switch ($action) { + case 'searchProduct': + $this->processSearchProduct(); + break; + case 'uploadImage': + $this->processUploadImage(); + break; + case 'deleteImage': + $this->processDeleteFreshImage(); + break; + default: + throw new Exception('Unknown action'); + } + } catch (Exception $e) { + $this->jsonResponse(['success' => false, 'message' => $e->getMessage()]); } - // No further processing needed for AJAX exit; } - /** - * Handles searching for a product by EAN13 barcode or ID. - */ - protected function ajaxProcessSearchProduct() + protected function processSearchProduct() { - $identifier = Tools::getValue('identifier'); + $identifier = trim(Tools::getValue('identifier')); if (empty($identifier)) { - $this->jsonError($this->trans('Identifier cannot be empty.',[], 'Modules.Addlivephoto.Admin')); + throw new Exception($this->trans('Please enter a barcode or ID.', [], 'Modules.Addlivephoto.Admin')); } $id_product = 0; - if (is_numeric($identifier)) { - // Check if it's an EAN or a Product ID - $id_product_by_ean = (int)Db::getInstance()->getValue(' - SELECT id_product FROM `' . _DB_PREFIX_ . 'product` WHERE ean13 = \'' . pSQL($identifier) . '\' - '); - if ($id_product_by_ean) { - $id_product = $id_product_by_ean; - } else { - // Assume it's a product ID if not found by EAN - $id_product = (int)$identifier; - } + // 1. Спробуємо знайти по EAN13 + $sql = 'SELECT id_product FROM `' . _DB_PREFIX_ . 'product` WHERE ean13 = "'.pSQL($identifier).'"'; + $id_by_ean = Db::getInstance()->getValue($sql); + + if ($id_by_ean) { + $id_product = (int)$id_by_ean; + } elseif (is_numeric($identifier)) { + // 2. Якщо це число, пробуємо як ID + $id_product = (int)$identifier; } - if (!$id_product || !Validate::isLoadedObject($product = new Product($id_product, false, $this->context->language->id))) { - $this->jsonError($this->trans('Product not found.',[], 'Modules.Addlivephoto.Admin')); + $product = new Product($id_product, false, $this->context->language->id); + + if (!Validate::isLoadedObject($product)) { + throw new Exception($this->trans('Product not found.', [], 'Modules.Addlivephoto.Admin')); } - // Get product prices - $retail_price = Product::getPriceStatic($product->id, true, null, 2, null, false, true); - $discounted_price = Product::getPriceStatic($product->id, true, null, 2, null, true, true); + // Отримуємо існуючі фото + $existing_photos = $this->getLivePhotos($product->id); - // Fetch existing live photos for this product - $live_photos = $this->getLivePhotosForProduct($product->id); - - $response = [ - 'id_product' => $product->id, - 'name' => $product->name, - 'wholesale_price' => $product->wholesale_price, - 'retail_price' => $retail_price, - 'discounted_price' => ($retail_price !== $discounted_price) ? $discounted_price : null, - 'existing_photos' => $live_photos, - ]; - - $this->jsonSuccess($response); + $this->jsonResponse([ + 'success' => true, + 'data' => [ + 'id_product' => $product->id, + 'name' => $product->name, + 'reference' => $product->reference, + 'ean13' => $product->ean13, + 'existing_photos' => $existing_photos + ] + ]); } - /** - * Handles the image upload process. - */ - protected function ajaxProcessUploadImage() + protected function processUploadImage() { $id_product = (int)Tools::getValue('id_product'); - $imageData = Tools::getValue('imageData'); + $rawImage = Tools::getValue('imageData'); // base64 string + $imageType = Tools::getValue('image_type'); // expiry або packaging - if (!$id_product || !$imageData) { - $this->jsonError($this->trans('Missing product ID or image data.',[], 'Modules.Addlivephoto.Admin')); + if (!$id_product || !$rawImage) { + throw new Exception('Missing ID or Image Data'); } - // Remove the data URI scheme header - list($type, $imageData) = explode(';', $imageData); - list(, $imageData) = explode(',', $imageData); - $imageData = base64_decode($imageData); - - if ($imageData === false) { - $this->jsonError($this->trans('Invalid image data.',[], 'Modules.Addlivephoto.Admin')); + if (!in_array($imageType, ['expiry', 'packaging'])) { + $imageType = 'expiry'; // Fallback } - $image_name = uniqid() . '.webp'; + // Clean Base64 + if (preg_match('/^data:image\/(\w+);base64,/', $rawImage, $type)) { + $rawImage = substr($rawImage, strpos($rawImage, ',') + 1); + $type = strtolower($type[1]); // jpg, png, webp + + if (!in_array($type, ['jpg', 'jpeg', 'png', 'webp'])) { + throw new Exception('Invalid image type'); + } + + $rawImage = base64_decode($rawImage); + if ($rawImage === false) { + throw new Exception('Base64 decode failed'); + } + } else { + throw new Exception('Did not match data URI with image data'); + } + + // Generate Filename + $filename = uniqid() . '.webp'; // Save as WebP usually $path = $this->module->getProductImageServerPath($id_product); - - if (!$path || !file_put_contents($path . $image_name, $imageData)) { - $this->jsonError($this->trans('Could not save image file. Check permissions for /var/modules/addlivephoto/',[], 'Modules.Addlivephoto.Admin')); + + if (!$path) { + throw new Exception('Could not create directory'); } - // Save to database - $success = Db::getInstance()->insert(AddLivePhoto::TABLE_NAME, [ + // Save File + if (!file_put_contents($path . $filename, $rawImage)) { + throw new Exception('Failed to write file to disk'); + } + + // Save to DB + $res = Db::getInstance()->insert('add_live_photo', [ 'id_product' => $id_product, - 'image_name' => pSQL($image_name), + 'image_name' => pSQL($filename), + 'image_type' => pSQL($imageType), 'date_add' => date('Y-m-d H:i:s'), ]); - if (!$success) { - // Clean up the created file if DB insert fails - @unlink($path . $image_name); - $this->jsonError($this->trans('Could not save image information to the database.',[], 'Modules.Addlivephoto.Admin')); + if (!$res) { + @unlink($path . $filename); // Cleanup + throw new Exception('Database insert error'); } - $new_photo_data = [ - 'name' => $image_name, - 'url' => $this->module->getProductImageUri($id_product, $image_name), - 'full_url' => $this->module->getProductImageUri($id_product, $image_name), - ]; + $photoUrl = $this->module->getProductImageUri($id_product, $filename); - $this->jsonSuccess(['message' => $this->trans('Image uploaded successfully!',[], 'Modules.Addlivephoto.Admin'), 'new_photo' => $new_photo_data]); + $this->jsonResponse([ + 'success' => true, + 'message' => 'Saved successfully!', + 'photo' => [ + 'name' => $filename, + 'url' => $photoUrl, + 'type' => $imageType + ] + ]); } - /** - * Handles deleting a specific image. - */ - protected function ajaxProcessDeleteImage() + protected function processDeleteFreshImage() { $id_product = (int)Tools::getValue('id_product'); $image_name = Tools::getValue('image_name'); - if (!$id_product || !$image_name) { - $this->jsonError($this->trans('Missing product ID or image name.',[], 'Modules.Addlivephoto.Admin')); - } - - // Use the method from the main module class if ($this->module->deleteProductImage($id_product, $image_name)) { - $this->jsonSuccess(['message' => $this->trans('Image deleted successfully.')]); + $this->jsonResponse(['success' => true, 'message' => 'Deleted']); } else { - $this->jsonError($this->trans('Failed to delete image.',[], 'Modules.Addlivephoto.Admin')); + throw new Exception('Delete failed'); } } - /** - * Fetches all live photos for a given product ID. - * @param int $id_product - * @return array - */ - private function getLivePhotosForProduct($id_product) + private function getLivePhotos($id_product) { $sql = new DbQuery(); - $sql->select('`image_name`'); - $sql->from(AddLivePhoto::TABLE_NAME); - $sql->where('`id_product` = ' . (int)$id_product); - $sql->orderBy('`date_add` DESC'); - - $results = Db::getInstance()->executeS($sql); - + $sql->select('*'); + $sql->from('add_live_photo'); + $sql->where('id_product = ' . (int)$id_product); + $sql->orderBy('date_add DESC'); + + $res = Db::getInstance()->executeS($sql); $photos = []; - if ($results) { - foreach ($results as $row) { + if ($res) { + foreach($res as $row) { $photos[] = [ 'name' => $row['image_name'], - 'url' => $this->module->getProductImageUri($id_product, $row['image_name']), + 'type' => isset($row['image_type']) ? $row['image_type'] : 'expiry', + 'url' => $this->module->getProductImageUri($id_product, $row['image_name']) ]; } } return $photos; } - /** Helper functions for consistent JSON responses */ - - private function jsonSuccess($data) + private function jsonResponse($data) { header('Content-Type: application/json'); - echo json_encode(['success' => true, 'data' => $data]); + echo json_encode($data); + exit; } -} +} \ No newline at end of file diff --git a/views/js/admin.js b/views/js/admin.js index e62a416..b76e06f 100644 --- a/views/js/admin.js +++ b/views/js/admin.js @@ -1,309 +1,254 @@ document.addEventListener('DOMContentLoaded', () => { - // --- DOM Element References --- - const videoContainer = document.getElementById('alp-video-container'); + // 1. Elements 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 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 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'); + 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 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; + // State let currentStream = null; let barcodeDetector = null; + let isScanning = false; 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(); - } + // 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); - updateUIForState(AppState.IDLE); // Set initial UI state + btnExpiry.addEventListener('click', () => takePhoto('expiry')); + btnPkg.addEventListener('click', () => takePhoto('packaging')); - // --- Event Listeners --- - videoContainer.addEventListener('click', handleViewfinderTap); - cameraSelector.addEventListener('change', handleCameraChange); - manualInputForm.addEventListener('submit', handleManualSubmit); - existingPhotosContainer.addEventListener('click', handleDeleteClick); + // --- Core Functions --- - // --- Core Logic --- - function handleViewfinderTap() { - switch (currentState) { - case AppState.IDLE: - startCamera(); - break; - case AppState.READY_TO_SCAN: - detectBarcode(); - break; - case AppState.PRODUCT_FOUND: - takePhoto(); - break; + 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."; } } - 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 = `
`; - 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 { + const constraints = { + video: { + facingMode: 'environment', // Rear camera + width: { ideal: 1280 }, + height: { ideal: 720 } + } + }; currentStream = await navigator.mediaDevices.getUserMedia(constraints); video.srcObject = currentStream; - await video.play(); - updateUIForState(AppState.READY_TO_SCAN); + + // 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('Error accessing camera:', err); - stopCamera(); // Ensure everything is reset - updateUIForState(AppState.IDLE, 'Camera Error. Tap to retry.'); + console.error(err); + overlayText.textContent = "Camera Access Denied or Error"; } } - function stopCamera() { - if (currentStream) { - currentStream.getTracks().forEach(track => track.stop()); - currentStream = null; - } - video.srcObject = null; - updateUIForState(AppState.IDLE); - } + async function scanLoop() { + if (!isScanning || !barcodeDetector || currentProductId) return; - 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); + const code = barcodes[0].rawValue; + isScanning = false; // Stop scanning + fetchProduct(code); + return; } - } catch (err) { - console.error('Barcode detection error:', err); - showMessage('Error during barcode detection.', true); - updateUIForState(AppState.READY_TO_SCAN); + } catch (e) { + // Detection error (common in loop) } - } - - 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); + // Scan every 200ms to save battery + setTimeout(() => requestAnimationFrame(scanLoop), 200); } - // --- 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); - } - } + function fetchProduct(identifier) { + overlayText.textContent = "Searching..."; + isScanning = false; - 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) */ } + const fd = new FormData(); + fd.append('action', 'searchProduct'); + fd.append('identifier', identifier); - // --- 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); + 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(); }); - 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 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 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 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 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 += ` | Discounted: ${product.discounted_price}`; - } - 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)); + + function renderPhotos(photos) { + photoListEl.innerHTML = ''; + if(photos && photos.length) { + photos.forEach(addPhotoToDom); } else { - existingPhotosSection.style.display = 'none'; + photoListEl.innerHTML = '

No photos yet.

'; } } - function appendNewPhoto(photo, productId = currentProductId) { - const thumbDiv = document.createElement('div'); - thumbDiv.className = 'photo-thumb'; - thumbDiv.innerHTML = ` - - Live photo - - + 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 = ` + + ${photo.type} + `; - existingPhotosContainer.prepend(thumbDiv); - existingPhotosSection.style.display = 'block'; + photoListEl.prepend(div); } - 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 + 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'); + } + }); + }; }); \ No newline at end of file diff --git a/views/templates/admin/uploader.tpl b/views/templates/admin/uploader.tpl index fcecd76..bc9c448 100644 --- a/views/templates/admin/uploader.tpl +++ b/views/templates/admin/uploader.tpl @@ -1,88 +1,557 @@ -{** - This script block passes the unique, secure AJAX URL from the PHP controller to our JavaScript. - The 'javascript' escaper is crucial to prevent encoding issues. -**} - -
+
- {l s='Live Photo Uploader' d='Modules.Addlivephoto.Admin'} + {l s='Live Photo Scanner' d='Modules.Addlivephoto.Admin'}
-
-
-
+
+
- {* --- The New Unified Camera Interface --- *} -
-
- {* The video feed will be attached here by JavaScript *} - + {* 1. CAMERA VIEW *} +
+
+ + - {* This overlay displays instructions and is the main tap target *} -
-
-
+ {* Camera Controls Overlay *} +
+ {* Flash Button (Hidden by default until capability detected) *} + - {* This canvas is used for capturing the frame but is not visible *} - + {* Zoom Button (Hidden by default) *} + +
+ + {* Overlay Message *} +
+ Starting Camera...
- {* --- Message Area (for non-critical feedback) --- *} - - - {* --- Product Information (hidden by default) --- *} - + + {* 2. PRODUCT ACTIONS (Hidden initially) *} + +
-
\ No newline at end of file +
+ + + + + \ No newline at end of file diff --git a/views/templates/hook/displayProductPriceBlock.tpl b/views/templates/hook/displayProductPriceBlock.tpl index 15b8664..a8eff09 100644 --- a/views/templates/hook/displayProductPriceBlock.tpl +++ b/views/templates/hook/displayProductPriceBlock.tpl @@ -1,228 +1,114 @@ -{* -* 2007-2023 PrestaShop -* -* NOTICE OF LICENSE -* -* This source file is subject to the Academic Free License (AFL 3.0) -* that is bundled with this package in the file LICENSE.txt. -* It is also available through the world-wide-web at this URL: -* http://opensource.org/licenses/afl-3.0.php -* If you did not receive a copy of the license and are unable to -* obtain it through the world-wide-web, please send an email -* to license@prestashop.com so we can send you a copy immediately. -* -* DISCLAIMER -* -* Do not edit or add to this file if you wish to upgrade PrestaShop to newer -* versions in the future. If you wish to customize PrestaShop for your -* needs please refer to http://www.prestashop.com for more information. -* -* @author Your Name -* @copyright 2007-2023 PrestaShop SA -* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0) -* International Registered Trademark & Property of PrestaShop SA -*} - {if isset($live_photos) && !empty($live_photos)} -
-
{l s='Freshness Guaranteed: See Today\'s Stock' d='Modules.Addlivephoto.Shop'}
+
+
+ verified + {l s='Live Warehouse Photos' d='Modules.Addlivephoto.Shop'} +
{foreach from=$live_photos item=photo name=livephotoloop} - - {$photo.alt|escape:'htmlall':'UTF-8'} - +
+ + {$photo.alt|escape:'htmlall':'UTF-8'} + + + {* BADGES *} + {if $photo.type == 'expiry'} + + {l s='Expiry Date' d='Modules.Addlivephoto.Shop'} + + {else} + + {l s='Packaging' d='Modules.Addlivephoto.Shop'} + + {/if} + + {* SCHEMA.ORG METADATA FOR GOOGLE (Hidden but readable by bots) *} +
+ + + +
+
{/foreach}
-
- {* --- MODAL --- *} -