File manager - Edit - /home/webapp69.cm.in.th/u69319090039/Shop39/public/app/Controllers/Seller/ProductManagementController.php
Back
<?php class ProductManagementController extends Controller { public function index(): void { $seller = Auth::sellerProfile(); $db = Database::getInstance(); $search = trim(Security::sanitizeString($_GET['q'] ?? '')); $statusFilter = trim(Security::sanitizeString($_GET['status'] ?? '')); $sql = " SELECT p.*, c.name AS category_name, (SELECT image_path FROM product_images WHERE product_id = p.id ORDER BY is_primary DESC, id ASC LIMIT 1) AS primary_image FROM products p JOIN categories c ON p.category_id = c.id WHERE p.seller_id = :sid "; $params = ['sid' => $seller['id']]; if (!empty($search)) { $sql .= " AND (p.title LIKE :search OR p.sku LIKE :search OR p.description LIKE :search)"; $params['search'] = "%{$search}%"; } if (!empty($statusFilter) && in_array($statusFilter, ['active', 'inactive'])) { $sql .= " AND p.status = :status"; $params['status'] = $statusFilter; } $sql .= " ORDER BY p.id DESC"; $stmt = $db->prepare($sql); $stmt->execute($params); $products = $stmt->fetchAll(); // Calculate counts $countStmt = $db->prepare(" SELECT COUNT(*) AS total, SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_count, SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) AS inactive_count, SUM(CASE WHEN stock_quantity <= 0 THEN 1 ELSE 0 END) AS out_of_stock_count FROM products WHERE seller_id = :sid "); $countStmt->execute(['sid' => $seller['id']]); $counts = $countStmt->fetch() ?: ['total' => 0, 'active_count' => 0, 'inactive_count' => 0, 'out_of_stock_count' => 0]; $this->render('seller/products/index', [ 'title' => 'จัดการสินค้า', 'products' => $products, 'search' => $search, 'statusFilter' => $statusFilter, 'counts' => $counts, 'csrf_token' => Session::get('csrf_token') ], 'seller'); } public function toggleStatus(string $id): void { $this->validateCsrf(); $seller = Auth::sellerProfile(); $productId = (int)$id; $productModel = new Product(); $product = $productModel->findById($productId); if ($product && (int)$product['seller_id'] === (int)$seller['id']) { $newStatus = ($product['status'] === 'active') ? 'inactive' : 'active'; $productModel->update($productId, ['status' => $newStatus]); $statusLabel = ($newStatus === 'active') ? 'เปิดการขาย' : 'ปิดการขาย'; Session::setFlash('success', "เปลี่ยนสถานะสินค้า \"{$product['title']}\" เป็น {$statusLabel} เรียบร้อยแล้ว"); } else { Session::setFlash('danger', 'ไม่พบสินค้าหรือไม่ได้รับอนุญาตให้แก้ไข'); } $this->redirect('seller/products'); } public function create(): void { $seller = Auth::sellerProfile(); if ($seller['status'] === 'suspended') { Session::setFlash('danger', 'ร้านค้าของคุณถูกระงับการใช้งานชั่วคราว ไม่สามารถเพิ่มสินค้าใหม่ได้'); $this->redirect('seller/products'); } $categoryModel = new Category(); $this->render('seller/products/create', [ 'title' => 'เพิ่มสินค้าใหม่', 'categories' => $categoryModel->getActiveCategories(), 'seller' => $seller ], 'seller'); } public function store(): void { $this->validateCsrf(); $seller = Auth::sellerProfile(); if ($seller['status'] === 'suspended') { Session::setFlash('danger', 'ร้านค้าของคุณถูกระงับการใช้งานชั่วคราว ไม่สามารถเพิ่มสินค้าใหม่ได้'); $this->redirect('seller/products'); } $title = Security::sanitizeString($_POST['title'] ?? ''); $categoryId = (int)($_POST['category_id'] ?? 0); $price = (float)($_POST['price'] ?? 0); $salePrice = !empty($_POST['sale_price']) ? (float)$_POST['sale_price'] : null; $stock = (int)($_POST['stock_quantity'] ?? 0); $sku = Security::sanitizeString($_POST['sku'] ?? ''); $barcode = Security::sanitizeString($_POST['barcode'] ?? ''); $description = Security::sanitizeString($_POST['description'] ?? ''); $applyWatermark = !empty($_POST['apply_watermark']); if (empty($title) || $categoryId <= 0 || $price <= 0 || empty($sku)) { Session::setFlash('danger', 'กรุณากรอกข้อมูลสินค้าและ SKU ให้ครบถ้วน'); $this->redirect('seller/products/create'); } $db = Database::getInstance(); // ตรวจสอบว่า SKU ซ้ำกับสินค้าอื่นในระบบหรือไม่ $checkSku = $db->prepare("SELECT id FROM products WHERE sku = :sku LIMIT 1"); $checkSku->execute(['sku' => $sku]); if ($checkSku->fetch()) { // ถ้าซ้ำ ให้สุ่มเลขต่อท้ายให้อัตโนมัติ เพื่อไม่ให้เกิด Error $sku = $sku . '-' . rand(100, 999); } // ตรวจสอบ upload errors และเก็บไฟล์ที่ valid $uploadedFiles = []; $uploadErrors = []; if (isset($_FILES['product_media']) && is_array($_FILES['product_media']['name'])) { $fileCount = count($_FILES['product_media']['name']); for ($i = 0; $i < $fileCount; $i++) { $errCode = $_FILES['product_media']['error'][$i]; if ($errCode === UPLOAD_ERR_OK) { $uploadedFiles[] = [ 'name' => $_FILES['product_media']['name'][$i], 'type' => $_FILES['product_media']['type'][$i], 'tmp_name' => $_FILES['product_media']['tmp_name'][$i], 'error' => $errCode, 'size' => $_FILES['product_media']['size'][$i] ]; } elseif ($errCode === UPLOAD_ERR_INI_SIZE || $errCode === UPLOAD_ERR_FORM_SIZE) { $uploadErrors[] = "ไฟล์ '{$_FILES['product_media']['name'][$i]}' มีขนาดเกินกำหนด (ไม่เกิน 10MB ต่อไฟล์)"; } elseif ($errCode === UPLOAD_ERR_NO_TMP_DIR) { $uploadErrors[] = "ระบบ Server ไม่มีโฟลเดอร์ชั่วคราวสำหรับ upload กรุณาติดต่อผู้ดูแลระบบ"; } elseif ($errCode !== UPLOAD_ERR_NO_FILE) { $uploadErrors[] = "ไฟล์ '{$_FILES['product_media']['name'][$i]}' upload ไม่สำเร็จ (error code: {$errCode})"; } } } // แสดง upload errors ก่อน redirect if (!empty($uploadErrors)) { Session::setFlash('danger', implode('<br>', $uploadErrors)); $this->redirect('seller/products/create'); } $validMediaCount = count($uploadedFiles); if ($validMediaCount < 3 || $validMediaCount > 15) { Session::setFlash('danger', "กรุณาอัปโหลดรูปภาพสินค้าอย่างน้อย 3 ไฟล์ และไม่เกิน 15 ไฟล์ (ได้รับ {$validMediaCount} ไฟล์)"); $this->redirect('seller/products/create'); } $productModel = new Product(); $slug = Security::slugify($title) . '-' . time(); $productId = $productModel->create([ 'seller_id' => $seller['id'], 'category_id' => $categoryId, 'title' => $title, 'slug' => $slug, 'sku' => $sku, 'barcode' => $barcode, 'description' => $description, 'price' => $price, 'sale_price' => $salePrice, 'stock_quantity' => $stock, 'status' => 'active' ]); // สร้างโฟลเดอร์ upload (พร้อม fallback permission) $uploadDir = SITE_ROOT . '/public/uploads/products/'; $this->ensureUploadDir($uploadDir); $db = Database::getInstance(); $imgStmt = $db->prepare("INSERT INTO product_images (product_id, image_path, is_primary) VALUES (:pid, :path, :is_primary)"); $uploadFailures = []; foreach ($uploadedFiles as $index => $file) { $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $filename = 'prod_' . $productId . '_' . time() . '_' . $index . '.' . $ext; $targetPath = $uploadDir . $filename; $dbRelPath = 'uploads/products/' . $filename; if (move_uploaded_file($file['tmp_name'], $targetPath)) { if ($applyWatermark && in_array($ext, ['jpg', 'jpeg', 'png', 'webp'])) { $this->applyWatermarkToImage($targetPath, $seller['shop_name']); } $isPrimary = ($index === 0) ? 1 : 0; $imgStmt->execute([ 'pid' => $productId, 'path' => $dbRelPath, 'is_primary' => $isPrimary ]); } else { $uploadFailures[] = $file['name']; error_log("[ProductUpload] move_uploaded_file failed: {$file['name']} → {$targetPath}"); } } // Handle Dynamic Product Options & Variants Creation $this->saveProductOptionsAndVariants($productId, $_POST, $_FILES); if (!empty($uploadFailures)) { Session::setFlash('warning', 'บันทึกสินค้าแล้ว แต่รูปภาพบางไฟล์ upload ไม่สำเร็จ: ' . implode(', ', $uploadFailures) . ' — กรุณาตรวจสอบสิทธิ์โฟลเดอร์ uploads/products/ บน hosting'); } else { Session::setFlash('success', 'เพิ่มสินค้าใหม่เรียบร้อยแล้ว'); } $this->redirect('seller/products'); } public function edit(string $id): void { $seller = Auth::sellerProfile(); if ($seller['status'] === 'suspended') { Session::setFlash('danger', 'ร้านค้าของคุณถูกระงับการใช้งานชั่วคราว ไม่สามารถแก้ไขสินค้าได้'); $this->redirect('seller/products'); } $productId = (int)$id; $productModel = new Product(); $productData = $productModel->getFullProductData($productId); if (!$productData || (int)$productData['seller_id'] !== (int)$seller['id']) { Session::setFlash('danger', 'ไม่พบสินค้าหรือไม่ได้รับอนุญาตให้แก้ไข'); $this->redirect('seller/products'); } $categoryModel = new Category(); $this->render('seller/products/edit', [ 'title' => 'แก้ไขสินค้า - ' . $productData['title'], 'product' => $productData, 'images' => $productData['images'] ?? [], 'options' => $productData['options'] ?? [], 'variants' => $productData['variants'] ?? [], 'categories' => $categoryModel->getActiveCategories(), 'seller' => $seller ], 'seller'); } public function update(string $id): void { $this->validateCsrf(); $seller = Auth::sellerProfile(); if ($seller['status'] === 'suspended') { Session::setFlash('danger', 'ร้านค้าของคุณถูกระงับการใช้งานชั่วคราว ไม่สามารถแก้ไขสินค้าได้'); $this->redirect('seller/products'); } $productId = (int)$id; $productModel = new Product(); $product = $productModel->findById($productId); if (!$product || (int)$product['seller_id'] !== (int)$seller['id']) { Session::setFlash('danger', 'ไม่พบสินค้าหรือไม่ได้รับอนุญาตให้แก้ไข'); $this->redirect('seller/products'); } $title = Security::sanitizeString($_POST['title'] ?? ''); $categoryId = (int)($_POST['category_id'] ?? 0); $price = (float)($_POST['price'] ?? 0); $salePrice = !empty($_POST['sale_price']) ? (float)$_POST['sale_price'] : null; $stock = (int)($_POST['stock_quantity'] ?? 0); $sku = Security::sanitizeString($_POST['sku'] ?? ''); $barcode = Security::sanitizeString($_POST['barcode'] ?? ''); $description = Security::sanitizeString($_POST['description'] ?? ''); $status = Security::sanitizeString($_POST['status'] ?? 'active'); $applyWatermark = !empty($_POST['apply_watermark']); if (empty($title) || $categoryId <= 0 || $price <= 0 || empty($sku)) { Session::setFlash('danger', 'กรุณากรอกข้อมูลสินค้าและ SKU ให้ครบถ้วน'); $this->redirect('seller/products/edit/' . $productId); } $db = Database::getInstance(); // ตรวจสอบ SKU ซ้ำกับสินค้าตัวอื่น (ยกเว้นตัวเอง) $checkSku = $db->prepare("SELECT id FROM products WHERE sku = :sku AND id != :pid LIMIT 1"); $checkSku->execute(['sku' => $sku, 'pid' => $productId]); if ($checkSku->fetch()) { Session::setFlash('danger', "รหัสสินค้า SKU '{$sku}' ถูกใช้งานแล้วโดยสินค้าอื่น กรุณาเปลี่ยนรหัส SKU ใหม่"); $this->redirect('seller/products/edit/' . $productId); } // Update main product details $productModel->update($productId, [ 'category_id' => $categoryId, 'title' => $title, 'sku' => $sku, 'barcode' => $barcode, 'description' => $description, 'price' => $price, 'sale_price' => $salePrice, 'stock_quantity' => $stock, 'status' => $status ]); $db = Database::getInstance(); // Handle deleted images if (!empty($_POST['delete_images']) && is_array($_POST['delete_images'])) { $delStmt = $db->prepare("DELETE FROM product_images WHERE id = :img_id AND product_id = :pid"); foreach ($_POST['delete_images'] as $imgId) { $delStmt->execute(['img_id' => (int)$imgId, 'pid' => $productId]); } } // Handle newly uploaded media files $uploadedFiles = []; if (isset($_FILES['product_media']) && is_array($_FILES['product_media']['name'])) { $fileCount = count($_FILES['product_media']['name']); for ($i = 0; $i < $fileCount; $i++) { if ($_FILES['product_media']['error'][$i] === UPLOAD_ERR_OK) { $uploadedFiles[] = [ 'name' => $_FILES['product_media']['name'][$i], 'type' => $_FILES['product_media']['type'][$i], 'tmp_name' => $_FILES['product_media']['tmp_name'][$i], 'error' => $_FILES['product_media']['error'][$i], 'size' => $_FILES['product_media']['size'][$i] ]; } } } if (!empty($uploadedFiles)) { $uploadDir = SITE_ROOT . '/public/uploads/products/'; $this->ensureUploadDir($uploadDir); // Check if product currently has a primary image $checkPrimary = $db->prepare("SELECT COUNT(*) FROM product_images WHERE product_id = :pid AND is_primary = 1"); $checkPrimary->execute(['pid' => $productId]); $hasPrimary = ($checkPrimary->fetchColumn() > 0); $imgStmt = $db->prepare("INSERT INTO product_images (product_id, image_path, is_primary) VALUES (:pid, :path, :is_primary)"); $uploadFailures = []; foreach ($uploadedFiles as $index => $file) { $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $filename = 'prod_' . $productId . '_' . time() . '_' . $index . '.' . $ext; $targetPath = $uploadDir . $filename; $dbRelPath = 'uploads/products/' . $filename; if (move_uploaded_file($file['tmp_name'], $targetPath)) { if ($applyWatermark && in_array($ext, ['jpg', 'jpeg', 'png', 'webp'])) { $this->applyWatermarkToImage($targetPath, $seller['shop_name']); } $isPrimary = (!$hasPrimary && $index === 0) ? 1 : 0; $imgStmt->execute([ 'pid' => $productId, 'path' => $dbRelPath, 'is_primary' => $isPrimary ]); } else { $uploadFailures[] = $file['name']; error_log("[ProductUpload] move_uploaded_file failed: {$file['name']} → {$targetPath}"); } } if (!empty($uploadFailures)) { Session::setFlash('warning', 'บันทึกแล้ว แต่รูปภาพบางไฟล์ upload ไม่สำเร็จ: ' . implode(', ', $uploadFailures)); } } // Re-save Options and Variants $this->saveProductOptionsAndVariants($productId, $_POST, $_FILES); Session::setFlash('success', 'บันทึกการแก้ไขข้อมูลสินค้าเรียบร้อยแล้ว'); $this->redirect('seller/products'); } /** * Helper to process dynamic product options and generated variants into DB tables */ private function saveProductOptionsAndVariants(int $productId, array $postData, array $filesData): void { $db = Database::getInstance(); // 1. Delete old options, option_values, variants, and variant_option_values for this product $db->prepare("DELETE FROM product_options WHERE product_id = :pid")->execute(['pid' => $productId]); // 2. Decode options payload JSON $optionsPayload = !empty($postData['options_payload']) ? json_decode($postData['options_payload'], true) : []; if (empty($optionsPayload) || !is_array($optionsPayload)) { return; } $optionValueIdMap = []; // Key: "OptionName:ValueName" => option_value_id $optStmt = $db->prepare("INSERT INTO product_options (product_id, option_name, sort_order) VALUES (:pid, :name, :sort)"); $valStmt = $db->prepare("INSERT INTO product_option_values (option_id, value, sort_order) VALUES (:oid, :val, :sort)"); foreach ($optionsPayload as $optIdx => $opt) { $optName = Security::sanitizeString($opt['name'] ?? ''); if (empty($optName)) continue; // Rule 12: Skip option without name $optStmt->execute([ 'pid' => $productId, 'name' => $optName, 'sort' => $optIdx ]); $optionId = (int)$db->lastInsertId(); if (!empty($opt['values']) && is_array($opt['values'])) { $seenValues = []; foreach ($opt['values'] as $valIdx => $rawVal) { $cleanVal = Security::sanitizeString($rawVal ?? ''); if (empty($cleanVal)) continue; // Rule 12: Skip empty value $lowerVal = strtolower($cleanVal); if (in_array($lowerVal, $seenValues)) continue; // Rule 12: Skip duplicate value $seenValues[] = $lowerVal; $valStmt->execute([ 'oid' => $optionId, 'val' => $cleanVal, 'sort' => $valIdx ]); $valId = (int)$db->lastInsertId(); $mapKey = $optName . '::' . $cleanVal; $optionValueIdMap[$mapKey] = $valId; } } } // 3. Save Variants and Link Option Values if (!empty($postData['variants']) && is_array($postData['variants'])) { $varStmt = $db->prepare("INSERT INTO product_variants (product_id, sku, price, sale_price, stock, image) VALUES (:pid, :sku, :price, :sale_price, :stock, :image)"); $vovStmt = $db->prepare("INSERT INTO variant_option_values (variant_id, option_value_id) VALUES (:vid, :ovid)"); $uploadDir = SITE_ROOT . '/public/uploads/products/variants/'; $this->ensureUploadDir($uploadDir); foreach ($postData['variants'] as $idx => $vData) { $varSku = !empty($vData['sku']) ? Security::sanitizeString($vData['sku']) : null; $varPrice = isset($vData['price']) && $vData['price'] !== '' ? (float)$vData['price'] : null; $varSalePrice = isset($vData['sale_price']) && $vData['sale_price'] !== '' ? (float)$vData['sale_price'] : null; $varStock = isset($vData['stock']) ? (int)$vData['stock'] : 0; $varImage = !empty($vData['existing_image']) ? Security::sanitizeString($vData['existing_image']) : null; // Handle single variant image upload if provided if (isset($filesData['variant_image_' . $idx]) && $filesData['variant_image_' . $idx]['error'] === UPLOAD_ERR_OK) { $file = $filesData['variant_image_' . $idx]; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); $filename = 'var_' . $productId . '_' . time() . '_' . $idx . '.' . $ext; $targetPath = $uploadDir . $filename; if (move_uploaded_file($file['tmp_name'], $targetPath)) { $varImage = 'uploads/products/variants/' . $filename; } } $varStmt->execute([ 'pid' => $productId, 'sku' => $varSku, 'price' => $varPrice, 'sale_price' => $varSalePrice, 'stock' => $varStock, 'image' => $varImage ]); $variantId = (int)$db->lastInsertId(); // Link Option Values if (!empty($vData['options_json'])) { $combArray = json_decode($vData['options_json'], true); if (is_array($combArray)) { foreach ($combArray as $cItem) { $mapKey = ($cItem['option_name'] ?? '') . '::' . ($cItem['value'] ?? ''); if (isset($optionValueIdMap[$mapKey])) { $vovStmt->execute([ 'vid' => $variantId, 'ovid' => $optionValueIdMap[$mapKey] ]); } } } } } } } /** * Helper: สร้างโฟลเดอร์ upload พร้อม fallback permission * บาง hosting ต้องการ 0775 หรือ 0777 ถ้า 0755 ไม่ได้ */ private function ensureUploadDir(string $dir): bool { if (is_dir($dir)) { return true; } // ลอง 0755 ก่อน, ถ้าไม่ได้ลอง 0775 และ 0777 foreach ([0755, 0775, 0777] as $perm) { if (@mkdir($dir, $perm, true)) { return true; } } error_log("[ProductUpload] Failed to create directory: {$dir}"); return false; } public function delete(string $id): void { $this->validateCsrf(); $seller = Auth::sellerProfile(); $productId = (int)$id; $productModel = new Product(); $product = $productModel->findById($productId); if ($product && $product['seller_id'] === $seller['id']) { $productModel->delete($productId); Session::setFlash('success', 'ลบสินค้าเรียบร้อยแล้ว'); } else { Session::setFlash('danger', 'ไม่พบสินค้าหรือไม่ได้รับอนุญาตให้ลบ'); } $this->redirect('seller/products'); } /** * Helper to overlay shop name watermark onto uploaded image files using GD */ private function applyWatermarkToImage(string $filePath, string $shopName): void { $info = @getimagesize($filePath); if (!$info) return; $mime = $info['mime']; $image = null; switch ($mime) { case 'image/jpeg': $image = @imagecreatefromjpeg($filePath); break; case 'image/png': $image = @imagecreatefrompng($filePath); break; case 'image/webp': $image = @imagecreatefromwebp($filePath); break; default: return; } if (!$image) return; $width = imagesx($image); $height = imagesy($image); $text = "© " . $shopName; $font = 5; // GD built-in font size $textWidth = imagefontwidth($font) * strlen($text); $textHeight = imagefontheight($font); $padding = 10; $boxWidth = $textWidth + ($padding * 2); $boxHeight = $textHeight + ($padding * 2); $x = $width - $boxWidth - 15; $y = $height - $boxHeight - 15; if ($x < 0) $x = 5; if ($y < 0) $y = 5; // Draw semi-transparent dark background box for high legibility $bgColor = imagecolorallocatealpha($image, 0, 0, 0, 45); // ~65% opaque imagefilledrectangle($image, (int)$x, (int)$y, (int)($x + $boxWidth), (int)($y + $boxHeight), $bgColor); // Draw white text $textColor = imagecolorallocate($image, 255, 255, 255); imagestring($image, $font, (int)($x + $padding), (int)($y + $padding), $text, $textColor); // Save image back switch ($mime) { case 'image/jpeg': imagejpeg($image, $filePath, 90); break; case 'image/png': imagepng($image, $filePath, 9); break; case 'image/webp': imagewebp($image, $filePath, 90); break; } imagedestroy($image); } }
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.24 |
proxy
|
phpinfo
|
Settings