<?php
if (!defined('DASHBOARD_ROOT')) {
    http_response_code(403);
    exit;
}

require_once '/var/www/shared/encryption.php';
require_once '/var/www/shared/image_processor.php';

$editorStoreId = (int)($wsStoreId ?? $_GET['id'] ?? $_GET['store'] ?? $_POST['store'] ?? 1);

$stores = [];
try {
    $stores = Database::fetchAll("SELECT id, domain, nickname, color_master FROM stores WHERE is_active = 1 ORDER BY id ASC");
} catch (\Throwable $e) {}

$action = $_GET['action'] ?? $_POST['action'] ?? 'list';

if ($action === 'save') {
    $editId = (int)($_POST['product_id'] ?? 0);
    $rawName = trim($_POST['name'] ?? '');
    $name = Encryption::isEncrypted($rawName) ? $rawName : Encryption::encrypt($rawName, 'product');

    $rawPrice = trim($_POST['base_price'] ?? '0');
    $price = Encryption::isEncrypted($rawPrice) ? $rawPrice : Encryption::encrypt((string)((float)$rawPrice), 'product');

    $qualityNames = [];
    if (!empty($_POST['qualities'])) {
        foreach ($_POST['qualities'] as $q) {
            $qn = trim($q['name'] ?? '');
            if ($qn !== '') $qualityNames[] = $qn;
        }
    }
    $qualityList = implode(', ', $qualityNames);
    $rawQuality = $qualityList;
    $quality = Encryption::isEncrypted($rawQuality) ? $rawQuality : Encryption::encrypt($rawQuality, 'product');

    $rawNotes = trim($_POST['notes'] ?? '');
    $notes = Encryption::isEncrypted($rawNotes) ? $rawNotes : Encryption::encrypt($rawNotes, 'product');

    $status = in_array($_POST['status'] ?? '', ['active', 'draft', 'archived']) ? $_POST['status'] : 'draft';
    $order = (int)($_POST['display_order'] ?? 0);

    $imagePath = $_POST['existing_image'] ?? '';

    if (!empty($_FILES['image']['tmp_name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
        $tempUploadDir = '/opt/noname/data';
        $finalUploadDir = '/var/www/public/uploads/products';
        $tempWebPath = ImageProcessor::process($_FILES['image'], $tempUploadDir);
        if ($tempWebPath) {
            $filename = basename($tempWebPath);
            $tempFile = $tempUploadDir . '/' . $filename;
            $finalFile = $finalUploadDir . '/' . $filename;
            shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus ' . escapeshellarg($tempFile) . ' ' . escapeshellarg($finalFile));
            $imagePath = 'uploads/products/' . $filename;
            $oldImg = $_POST['existing_image'] ?? '';
            if ($oldImg) {
                $oldFile = '/var/www/public/' . ltrim($oldImg, '/');
                shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus_del ' . escapeshellarg($oldFile));
            }
        }
    }

    if ($editId > 0) {
        Database::execute(
            "UPDATE products SET name=:n, title=:t, image_path=:img, image_url=:img2, base_price=:bp, price=:pr, quality=:q, notes=:nt, description=:desc, short_desc=:sdesc, status=:s, status_code=:sc, display_order=:o, updated_at=NOW() WHERE id=:id",
            [':n'=>$name, ':t'=>$name, ':img'=>$imagePath, ':img2'=>$imagePath, ':bp'=>$price, ':pr'=>$rawPrice, ':q'=>$quality, ':nt'=>$notes, ':desc'=>$notes, ':sdesc'=>Encryption::encrypt(substr($rawNotes, 0, 100), 'product'), ':s'=>$status, ':sc'=>($status === 'active' ? 3002 : 3001), ':o'=>$order, ':id'=>$editId]
        );
        $pid = $editId;
    } else {
        $pid = Database::insert(
            "INSERT INTO products (store_id, name, title, image_path, image_url, base_price, price, quality, notes, description, short_desc, status, status_code, visibility, display_order, sku) VALUES (:store_id, :n, :t, :img, :img2, :bp, :pr, :q, :nt, :desc, :sdesc, :s, :sc, 'public', :o, :sku)",
            [':store_id'=>$editorStoreId, ':n'=>$name, ':t'=>$name, ':img'=>$imagePath, ':img2'=>$imagePath, ':bp'=>$price, ':pr'=>$rawPrice, ':q'=>$quality, ':nt'=>$notes, ':desc'=>$notes, ':sdesc'=>Encryption::encrypt(substr($rawNotes, 0, 100), 'product'), ':s'=>$status, ':sc'=>($status === 'active' ? 3002 : 3001), ':o'=>$order, ':sku'=>'CP-' . strtoupper(bin2hex(random_bytes(4)))]
        );
    }

    $typeUploadsDir = '/var/www/public/uploads/products';

    // Handle product types
    $types = $_POST['types'] ?? [];
    $keepTypeIds = [];
    foreach ($types as $idx => $t) {
        $typeName = trim($t['name'] ?? '');
        if ($typeName === '') continue;
        $typePrice = (float)($t['price'] ?? 0);
        $displayOrder = (int)($t['display_order'] ?? 0);
        $isActive = isset($t['active']) ? 1 : 0;
        $typeId = (int)($t['id'] ?? 0);

        $nameEnc = Encryption::isEncrypted($typeName) ? $typeName : Encryption::encrypt($typeName, 'product');
        $priceEnc = Encryption::isEncrypted((string)$typePrice) ? (string)$typePrice : Encryption::encrypt((string)$typePrice, 'product');

        $imageUrl = '';
        $fileKey = 'type_image_' . $idx;
        if (!empty($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
            $tempUploadDir = '/opt/noname/data';
            $finalUploadDir = '/var/www/public/uploads/products';
            $tempWebPath = ImageProcessor::process($_FILES[$fileKey], $tempUploadDir);
            if ($tempWebPath) {
                $filename = basename($tempWebPath);
                $tempFile = $tempUploadDir . '/' . $filename;
                $finalFile = $finalUploadDir . '/' . $filename;
                shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus ' . escapeshellarg($tempFile) . ' ' . escapeshellarg($finalFile));
                $imageUrl = 'uploads/products/' . $filename;
            }
        }

        if ($typeId > 0) {
            if ($imageUrl !== '') {
                $oldImg = Database::scalar("SELECT image_path FROM product_types WHERE id = :id", [':id' => $typeId]);
                if ($oldImg) {
                    $oldFile = '/var/www/public/' . ltrim($oldImg, '/');
                    shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus_del ' . escapeshellarg($oldFile));
                }
                Database::execute(
                    "UPDATE product_types SET type_name = :name, base_price = :price, image_path = :img, display_order = :order, is_active = :active WHERE id = :id",
                    [':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive, ':id' => $typeId]
                );
            } else {
                Database::execute(
                    "UPDATE product_types SET type_name = :name, base_price = :price, display_order = :order, is_active = :active WHERE id = :id",
                    [':name' => $nameEnc, ':price' => $priceEnc, ':order' => $displayOrder, ':active' => $isActive, ':id' => $typeId]
                );
            }
            $keepTypeIds[] = $typeId;
        } else {
            $newId = Database::insert(
                "INSERT INTO product_types (product_id, type_name, base_price, image_path, display_order, is_active) VALUES (:pid, :name, :price, :img, :order, :active)",
                [':pid' => $pid, ':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive]
            );
            if ($newId > 0) $keepTypeIds[] = $newId;
        }
    }
    $allTypes = Database::fetchAll("SELECT id, image_path FROM product_types WHERE product_id = :pid", [':pid' => $pid]);
    foreach ($allTypes as $t) {
        if (!in_array((int)$t['id'], $keepTypeIds)) {
            if ($t['image_path']) {
                ImageProcessor::delete($t['image_path']);
            }
            Database::execute("DELETE FROM product_types WHERE id = :id", [':id' => $t['id']]);
        }
    }

    // Handle product qualities
    $qualities = $_POST['qualities'] ?? [];
    $keepQualityIds = [];
    foreach ($qualities as $idx => $q) {
        $qName = trim($q['name'] ?? '');
        if ($qName === '') continue;
        $qPrice = (float)($q['price'] ?? 0);
        $displayOrder = (int)($q['display_order'] ?? 0);
        $isActive = isset($q['active']) ? 1 : 0;
        $qId = (int)($q['id'] ?? 0);

        $nameEnc = Encryption::isEncrypted($qName) ? $qName : Encryption::encrypt($qName, 'product');
        $priceEnc = Encryption::isEncrypted((string)$qPrice) ? (string)$qPrice : Encryption::encrypt((string)$qPrice, 'product');

        $imageUrl = '';
        $fileKey = 'quality_image_' . $idx;
        if (!empty($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) {
            $tempUploadDir = '/opt/noname/data';
            $finalUploadDir = '/var/www/public/uploads/products';
            $tempWebPath = ImageProcessor::process($_FILES[$fileKey], $tempUploadDir);
            if ($tempWebPath) {
                $filename = basename($tempWebPath);
                $tempFile = $tempUploadDir . '/' . $filename;
                $finalFile = $finalUploadDir . '/' . $filename;
                shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus ' . escapeshellarg($tempFile) . ' ' . escapeshellarg($finalFile));
                $imageUrl = 'uploads/products/' . $filename;
            }
        }

        if ($qId > 0) {
            if ($imageUrl !== '') {
                $oldImg = Database::scalar("SELECT image_path FROM product_qualities WHERE id = :id", [':id' => $qId]);
                if ($oldImg) {
                    $oldFile = '/var/www/public/' . ltrim($oldImg, '/');
                    shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus_del ' . escapeshellarg($oldFile));
                }
                Database::execute(
                    "UPDATE product_qualities SET quality_name = :name, base_price = :price, image_path = :img, display_order = :order, is_active = :active WHERE id = :id",
                    [':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive, ':id' => $qId]
                );
            } else {
                Database::execute(
                    "UPDATE product_qualities SET quality_name = :name, base_price = :price, display_order = :order, is_active = :active WHERE id = :id",
                    [':name' => $nameEnc, ':price' => $priceEnc, ':order' => $displayOrder, ':active' => $isActive, ':id' => $qId]
                );
            }
            $keepQualityIds[] = $qId;
        } else {
            $newId = Database::insert(
                "INSERT INTO product_qualities (product_id, quality_name, base_price, image_path, display_order, is_active) VALUES (:pid, :name, :price, :img, :order, :active)",
                [':pid' => $pid, ':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive]
            );
            if ($newId > 0) $keepQualityIds[] = $newId;
        }
    }
    $allQualities = Database::fetchAll("SELECT id, image_path FROM product_qualities WHERE product_id = :pid", [':pid' => $pid]);
    foreach ($allQualities as $q) {
        if (!in_array((int)$q['id'], $keepQualityIds)) {
            if ($q['image_path']) {
                ImageProcessor::delete($q['image_path']);
            }
            Database::execute("DELETE FROM product_qualities WHERE id = :id", [':id' => $q['id']]);
        }
    }

    // Handle product units
    $units = $_POST['units'] ?? [];
    $keepUnitIds = [];
    foreach ($units as $idx => $u) {
        $uAmount = trim($u['amount'] ?? '');
        if ($uAmount === '') continue;
        $uPrice = (float)($u['price'] ?? 1.0);
        $displayOrder = (int)($u['display_order'] ?? 0);
        $uId = (int)($u['id'] ?? 0);

        $amountEnc = Encryption::isEncrypted($uAmount) ? $uAmount : Encryption::encrypt($uAmount, 'product');
        $priceEnc = Encryption::isEncrypted((string)$uPrice) ? (string)$uPrice : Encryption::encrypt((string)$uPrice, 'product');

        if ($uId > 0) {
            Database::execute(
                "UPDATE product_units SET unit_amount = :amount, unit_price = :price, display_order = :order WHERE id = :id",
                [':amount' => $amountEnc, ':price' => $priceEnc, ':order' => $displayOrder, ':id' => $uId]
            );
            $keepUnitIds[] = $uId;
        } else {
            $newId = Database::insert(
                "INSERT INTO product_units (product_id, unit_amount, unit_price, display_order) VALUES (:pid, :amount, :price, :order)",
                [':pid' => $pid, ':amount' => $amountEnc, ':price' => $priceEnc, ':order' => $displayOrder]
            );
            if ($newId > 0) $keepUnitIds[] = $newId;
        }
    }
    $allUnits = Database::fetchAll("SELECT id FROM product_units WHERE product_id = :pid", [':pid' => $pid]);
    foreach ($allUnits as $u) {
        if (!in_array((int)$u['id'], $keepUnitIds)) {
            Database::execute("DELETE FROM product_units WHERE id = :id", [':id' => $u['id']]);
        }
    }

    header('Location: ?view=store&id=' . $editorStoreId . '&action=products&saved=1');
    exit;
}

if ($action === 'delete') {
    $delId = (int)($_POST['delete_id'] ?? 0);
    if ($delId > 0) {
        $row = Database::fetchOne("SELECT image_path FROM products WHERE id = :id", [':id'=>$delId]);
        if ($row && $row['image_path']) {
            $oldFile = '/var/www/public/' . ltrim($row['image_path'], '/');
            shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus_del ' . escapeshellarg($oldFile));
        }
        Database::execute("DELETE FROM products WHERE id = :id", [':id'=>$delId]);
        
        $types = Database::fetchAll("SELECT image_path FROM product_types WHERE product_id = :id", [':id'=>$delId]);
        foreach ($types as $t) {
            if ($t['image_path']) {
                $oldFile = '/var/www/public/' . ltrim($t['image_path'], '/');
                shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus_del ' . escapeshellarg($oldFile));
            }
        }
        Database::execute("DELETE FROM product_types WHERE product_id = :id", [':id'=>$delId]);

        $qualities = Database::fetchAll("SELECT image_path FROM product_qualities WHERE product_id = :id", [':id'=>$delId]);
        foreach ($qualities as $q) {
            if ($q['image_path']) {
                $oldFile = '/var/www/public/' . ltrim($q['image_path'], '/');
                shell_exec('sudo -n -u s3rv1c3agent /opt/noname/scripts/event_bus_del ' . escapeshellarg($oldFile));
            }
        }
        Database::execute("DELETE FROM product_qualities WHERE product_id = :id", [':id'=>$delId]);

        Database::execute("DELETE FROM product_units WHERE product_id = :id", [':id'=>$delId]);
    }
    header('Location: ?view=store&id=' . $editorStoreId . '&action=products&deleted=1');
    exit;
}

// ... Rest of the file will just be the UI HTML part.
$action = $_GET['action'] ?? $_POST['action'] ?? 'list'; if ($action === 'save') { file_put_contents('/var/www/dashboard/upload_debug.log', "POST: " . print_r($_POST, true) . "\nFILES: " . print_r($_FILES, true), FILE_APPEND); $editId = (int)($_POST['product_id'] ?? 0); $rawName = trim($_POST['name'] ?? ''); $name = Encryption::encrypt($rawName, 'product'); $rawPrice = (float)trim($_POST['base_price'] ?? '0'); $price = Encryption::encrypt((string)$rawPrice, 'product'); $qualityNames = []; if (!empty($_POST['qualities'])) { foreach ($_POST['qualities'] as $q) { $qn = trim($q['name'] ?? ''); if ($qn !== '') $qualityNames[] = $qn; } } $qualityList = implode(', ', $qualityNames); $rawQuality = $qualityList; $quality = Encryption::encrypt($rawQuality, 'product'); $rawNotes = trim($_POST['notes'] ?? ''); $notes = Encryption::encrypt($rawNotes, 'product'); $status = in_array($_POST['status'] ?? '', ['active','draft','archived']) ? $_POST['status'] : 'draft'; $order = (int)($_POST['display_order'] ?? 0); $imagePath = $_POST['existing_image'] ?? ''; if (!empty($_FILES['image']['tmp_name']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) { $uploadDir = '/var/www/public/uploads/products/'; if (!is_dir($uploadDir)) mkdir($uploadDir, 0775, true); $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $_FILES['image']['tmp_name']); finfo_close($finfo); $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp']; if (isset($allowed[$mime])) { $ext = $allowed[$mime]; $obfName = bin2hex(random_bytes(16)) . '.' . $ext; $destPath = $uploadDir . $obfName; try { if (class_exists('Imagick')) { $imagick = new Imagick($_FILES['image']['tmp_name']); $imagick->stripImage(); if ($mime === 'image/jpeg') { $imagick->setImageCompressionQuality(92); } elseif ($mime === 'image/png') { $imagick->setOption('png:compression-level', '6'); } else { $imagick->setImageCompressionQuality(85); } $imagick->writeImage($destPath); $imagick->clear(); $imagick->destroy(); } else { if ($mime === 'image/jpeg') { $img = @imagecreatefromjpeg($_FILES['image']['tmp_name']); if ($img) { imagejpeg($img, $destPath, 92); imagedestroy($img); } } elseif ($mime === 'image/png') { $img = @imagecreatefrompng($_FILES['image']['tmp_name']); if ($img) { imagesavealpha($img, true); imagepng($img, $destPath, 6); imagedestroy($img); } } else { $img = @imagecreatefromwebp($_FILES['image']['tmp_name']); if ($img) { imagewebp($img, $destPath, 85); imagedestroy($img); } } } } catch (\Throwable $e) {} if (file_exists($destPath)) { @chown($destPath, 'nginx'); @chmod($destPath, 0664); $imagePath = 'uploads/products/' . $obfName; $oldImg = $_POST['existing_image'] ?? ''; if ($oldImg && file_exists('/var/www/public/' . $oldImg)) { @unlink('/var/www/public/' . $oldImg); } } } } if ($editId > 0) { Database::execute( "UPDATE products SET name=:n, title=:t, image_path=:img, image_url=:img2, base_price=:bp, price=:pr, quality=:q, notes=:nt, description=:desc, short_desc=:sdesc, status=:s, status_code=:sc, display_order=:o, updated_at=NOW() WHERE id=:id", [':n'=>$name, ':t'=>$name, ':img'=>$imagePath, ':img2'=>$imagePath, ':bp'=>$price, ':pr'=>$rawPrice, ':q'=>$quality, ':nt'=>$notes, ':desc'=>$notes, ':sdesc'=>Encryption::encrypt(substr($rawNotes, 0, 100), 'product'), ':s'=>$status, ':sc'=>($status === 'active' ? 3002 : 3001), ':o'=>$order, ':id'=>$editId] ); $pid = $editId; } else { $pid = Database::insert( "INSERT INTO products (store_id, name, title, image_path, image_url, base_price, price, quality, notes, description, short_desc, status, status_code, visibility, display_order, sku) VALUES (:store_id, :n, :t, :img, :img2, :bp, :pr, :q, :nt, :desc, :sdesc, :s, :sc, 'public', :o, :sku)", [':store_id'=>$editorStoreId, ':n'=>$name, ':t'=>$name, ':img'=>$imagePath, ':img2'=>$imagePath, ':bp'=>$price, ':pr'=>$rawPrice, ':q'=>$quality, ':nt'=>$notes, ':desc'=>$notes, ':sdesc'=>Encryption::encrypt(substr($rawNotes, 0, 100), 'product'), ':s'=>$status, ':sc'=>($status === 'active' ? 3002 : 3001), ':o'=>$order, ':sku'=>'CP-' . strtoupper(bin2hex(random_bytes(4)))] ); } $typeUploadsDir = '/var/www/public/uploads/products'; if (!is_dir($typeUploadsDir)) @mkdir($typeUploadsDir, 0755, true); $types = $_POST['types'] ?? []; $keepTypeIds = []; foreach ($types as $idx => $t) { $typeName = trim($t['name'] ?? ''); if ($typeName === '') continue; $typePrice = (float)($t['price'] ?? 0); $displayOrder = (int)($t['display_order'] ?? 0); $isActive = isset($t['active']) ? 1 : 0; $typeId = (int)($t['id'] ?? 0); $nameEnc = Encryption::encrypt($typeName, 'product'); $priceEnc = Encryption::encrypt((string)$typePrice, 'product'); $imageUrl = ''; $fileKey = 'type_image_' . $idx; if (!empty($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) { $tmpPath = $_FILES[$fileKey]['tmp_name']; $ext = strtolower(pathinfo($_FILES[$fileKey]['name'], PATHINFO_EXTENSION)); $filename = 'var_' . $pid . '_type_' . $idx . '_' . time() . '.' . $ext; $destPath = rtrim($typeUploadsDir, '/') . '/' . $filename; $processed = false; try { if (class_exists('Imagick')) { $imagick = new Imagick($tmpPath); $imagick->stripImage(); if (in_array($ext, ['jpg', 'jpeg'])) $imagick->setImageCompressionQuality(92); elseif ($ext === 'png') $imagick->setOption('png:compression-level', '6'); elseif ($ext === 'webp') $imagick->setImageCompressionQuality(88); $imagick->writeImage($destPath); $imagick->clear(); $imagick->destroy(); $processed = true; } else { if (in_array($ext, ['jpg', 'jpeg'])) { $img = @imagecreatefromjpeg($tmpPath); if ($img) { imagejpeg($img, $destPath, 92); imagedestroy($img); $processed = true; } } elseif ($ext === 'png') { $img = @imagecreatefrompng($tmpPath); if ($img) { imagesavealpha($img, true); imagepng($img, $destPath, 6); imagedestroy($img); $processed = true; } } elseif ($ext === 'webp') { $img = @imagecreatefromwebp($tmpPath); if ($img) { imagewebp($img, $destPath, 88); imagedestroy($img); $processed = true; } } } } catch (\Throwable $e) {} if ($processed && file_exists($destPath)) { @chown($destPath, 'nginx'); @chmod($destPath, 0664); $imageUrl = '/uploads/products/' . $filename; } } if ($typeId > 0) { if ($imageUrl !== '') { $oldImg = Database::scalar("SELECT image_path FROM product_types WHERE id = :id", [':id' => $typeId]); if ($oldImg && file_exists('/var/www/public' . $oldImg)) { @unlink('/var/www/public' . $oldImg); } Database::execute( "UPDATE product_types SET type_name = :name, base_price = :price, image_path = :img, display_order = :order, is_active = :active WHERE id = :id", [':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive, ':id' => $typeId] ); } else { Database::execute( "UPDATE product_types SET type_name = :name, base_price = :price, display_order = :order, is_active = :active WHERE id = :id", [':name' => $nameEnc, ':price' => $priceEnc, ':order' => $displayOrder, ':active' => $isActive, ':id' => $typeId] ); } $keepTypeIds[] = $typeId; } else { $newId = Database::insert( "INSERT INTO product_types (product_id, type_name, base_price, image_path, display_order, is_active) VALUES (:pid, :name, :price, :img, :order, :active)", [':pid' => $pid, ':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive] ); if ($newId > 0) $keepTypeIds[] = $newId; } } $allTypes = Database::fetchAll("SELECT id, image_path FROM product_types WHERE product_id = :pid", [':pid' => $pid]); foreach ($allTypes as $t) { if (!in_array((int)$t['id'], $keepTypeIds)) { if ($t['image_path'] && file_exists('/var/www/public' . $t['image_path'])) { @unlink('/var/www/public' . $t['image_path']); } Database::execute("DELETE FROM product_types WHERE id = :id", [':id' => $t['id']]); } } $qualities = $_POST['qualities'] ?? []; $keepQualityIds = []; foreach ($qualities as $idx => $q) { $qName = trim($q['name'] ?? ''); if ($qName === '') continue; $qPrice = (float)($q['price'] ?? 0); $displayOrder = (int)($q['display_order'] ?? 0); $isActive = isset($q['active']) ? 1 : 0; $qId = (int)($q['id'] ?? 0); $nameEnc = Encryption::encrypt($qName, 'product'); $priceEnc = Encryption::encrypt((string)$qPrice, 'product'); $imageUrl = ''; $fileKey = 'quality_image_' . $idx; if (!empty($_FILES[$fileKey]) && $_FILES[$fileKey]['error'] === UPLOAD_ERR_OK) { $tmpPath = $_FILES[$fileKey]['tmp_name']; $ext = strtolower(pathinfo($_FILES[$fileKey]['name'], PATHINFO_EXTENSION)); $filename = 'var_' . $pid . '_quality_' . $idx . '_' . time() . '.' . $ext; $destPath = rtrim($typeUploadsDir, '/') . '/' . $filename; $processed = false; try { if (class_exists('Imagick')) { $imagick = new Imagick($tmpPath); $imagick->stripImage(); if (in_array($ext, ['jpg', 'jpeg'])) $imagick->setImageCompressionQuality(92); elseif ($ext === 'png') $imagick->setOption('png:compression-level', '6'); elseif ($ext === 'webp') $imagick->setImageCompressionQuality(88); $imagick->writeImage($destPath); $imagick->clear(); $imagick->destroy(); $processed = true; } else { if (in_array($ext, ['jpg', 'jpeg'])) { $img = @imagecreatefromjpeg($tmpPath); if ($img) { imagejpeg($img, $destPath, 92); imagedestroy($img); $processed = true; } } elseif ($ext === 'png') { $img = @imagecreatefrompng($tmpPath); if ($img) { imagesavealpha($img, true); imagepng($img, $destPath, 6); imagedestroy($img); $processed = true; } } elseif ($ext === 'webp') { $img = @imagecreatefromwebp($tmpPath); if ($img) { imagewebp($img, $destPath, 88); imagedestroy($img); $processed = true; } } } } catch (\Throwable $e) {} if ($processed && file_exists($destPath)) { @chown($destPath, 'nginx'); @chmod($destPath, 0664); $imageUrl = '/uploads/products/' . $filename; } } if ($qId > 0) { if ($imageUrl !== '') { $oldImg = Database::scalar("SELECT image_path FROM product_qualities WHERE id = :id", [':id' => $qId]); if ($oldImg && file_exists('/var/www/public' . $oldImg)) { @unlink('/var/www/public' . $oldImg); } Database::execute( "UPDATE product_qualities SET quality_name = :name, base_price = :price, image_path = :img, display_order = :order, is_active = :active WHERE id = :id", [':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive, ':id' => $qId] ); } else { Database::execute( "UPDATE product_qualities SET quality_name = :name, base_price = :price, display_order = :order, is_active = :active WHERE id = :id", [':name' => $nameEnc, ':price' => $priceEnc, ':order' => $displayOrder, ':active' => $isActive, ':id' => $qId] ); } $keepQualityIds[] = $qId; } else { $newId = Database::insert( "INSERT INTO product_qualities (product_id, quality_name, base_price, image_path, display_order, is_active) VALUES (:pid, :name, :price, :img, :order, :active)", [':pid' => $pid, ':name' => $nameEnc, ':price' => $priceEnc, ':img' => $imageUrl, ':order' => $displayOrder, ':active' => $isActive] ); if ($newId > 0) $keepQualityIds[] = $newId; } } $allQualities = Database::fetchAll("SELECT id, image_path FROM product_qualities WHERE product_id = :pid", [':pid' => $pid]); foreach ($allQualities as $q) { if (!in_array((int)$q['id'], $keepQualityIds)) { if ($q['image_path'] && file_exists('/var/www/public' . $q['image_path'])) { @unlink('/var/www/public' . $q['image_path']); } Database::execute("DELETE FROM product_qualities WHERE id = :id", [':id' => $q['id']]); } } $units = $_POST['units'] ?? []; $keepUnitIds = []; foreach ($units as $idx => $u) { $uAmount = trim($u['amount'] ?? ''); if ($uAmount === '') continue; $uPrice = (float)($u['price'] ?? 1.0); $displayOrder = (int)($u['display_order'] ?? 0); $uId = (int)($u['id'] ?? 0); $amountEnc = Encryption::encrypt($uAmount, 'product'); $priceEnc = Encryption::encrypt((string)$uPrice, 'product'); if ($uId > 0) { Database::execute( "UPDATE product_units SET unit_amount = :amount, unit_price = :price, display_order = :order WHERE id = :id", [':amount' => $amountEnc, ':price' => $priceEnc, ':order' => $displayOrder, ':id' => $uId] ); $keepUnitIds[] = $uId; } else { $newId = Database::insert( "INSERT INTO product_units (product_id, unit_amount, unit_price, display_order) VALUES (:pid, :amount, :price, :order)", [':pid' => $pid, ':amount' => $amountEnc, ':price' => $priceEnc, ':order' => $displayOrder] ); if ($newId > 0) $keepUnitIds[] = $newId; } } $allUnits = Database::fetchAll("SELECT id FROM product_units WHERE product_id = :pid", [':pid' => $pid]); foreach ($allUnits as $u) { if (!in_array((int)$u['id'], $keepUnitIds)) { Database::execute("DELETE FROM product_units WHERE id = :id", [':id' => $u['id']]); } } header('Location: ?view=store&id=' . $editorStoreId . '&action=products&saved=1'); exit; } if ($action === 'delete') { $delId = (int)($_POST['delete_id'] ?? 0); if ($delId > 0) { $row = Database::fetchOne("SELECT image_path FROM products WHERE id = :id", [':id'=>$delId]); if ($row && $row['image_path'] && file_exists('/var/www/public/' . $row['image_path'])) { @unlink('/var/www/public/' . $row['image_path']); } Database::execute("DELETE FROM products WHERE id = :id", [':id'=>$delId]); $types = Database::fetchAll("SELECT image_path FROM product_types WHERE product_id = :id", [':id'=>$delId]); foreach ($types as $t) { if ($t['image_path'] && file_exists('/var/www/public' . $t['image_path'])) { @unlink('/var/www/public' . $t['image_path']); } } Database::execute("DELETE FROM product_types WHERE product_id = :id", [':id'=>$delId]); $qualities = Database::fetchAll("SELECT image_path FROM product_qualities WHERE product_id = :id", [':id'=>$delId]); foreach ($qualities as $q) { if ($q['image_path'] && file_exists('/var/www/public' . $q['image_path'])) { @unlink('/var/www/public' . $q['image_path']); } } Database::execute("DELETE FROM product_qualities WHERE product_id = :id", [':id'=>$delId]); Database::execute("DELETE FROM product_units WHERE product_id = :id", [':id'=>$delId]); } header('Location: ?view=store&id=' . $editorStoreId . '&action=products&deleted=1'); exit; } $action = $_GET['action'] ?? 'list'; $editId = (int)($_GET['id'] ?? 0); $products = Database::fetchAll("SELECT * FROM products WHERE store_id = :sid ORDER BY display_order ASC, id DESC", [':sid' => $editorStoreId]); function _safeDec($v) { if (!$v) return ''; try { $d = Encryption::render($v); return ($d !== '' && $d !== false) ? $d : $v; } catch (\Throwable $e) { return $v; } } foreach ($products as &$p) { $p['_name'] = _safeDec($p['name'] ?? '') ?: _safeDec($p['title'] ?? ''); $p['_price'] = $p['price'] ?? _safeDec($p['base_price'] ?? ''); $p['_quality'] = _safeDec($p['quality'] ?? ''); $p['_enc_name'] = substr($p['name'] ?? $p['title'] ?? '', 0, 32); $p['_enc_price'] = substr($p['base_price'] ?? '', 0, 24); } unset($p); $editProduct = null; $editUnits = []; $editQualities = []; $editTypes = []; if ($action === 'edit' && $editId > 0) { $editProduct = Database::fetchOne("SELECT * FROM products WHERE id = :id", [':id'=>$editId]); if ($editProduct) { $editProduct['_name'] = _safeDec($editProduct['name'] ?? '') ?: _safeDec($editProduct['title'] ?? ''); $editProduct['_price'] = $editProduct['price'] ?? _safeDec($editProduct['base_price'] ?? ''); $editProduct['_quality'] = _safeDec($editProduct['quality'] ?? ''); $editProduct['_notes'] = _safeDec($editProduct['notes'] ?? '') ?: _safeDec($editProduct['description'] ?? ''); $editUnits = Database::fetchAll("SELECT * FROM product_units WHERE product_id = :pid ORDER BY display_order ASC, id ASC", [':pid'=>$editId]); foreach ($editUnits as &$u) { $u['unit_amount'] = _safeDec($u['unit_amount'] ?? ''); $u['unit_price'] = (float)(_safeDec($u['unit_price'] ?? '') ?: $u['unit_price']); } unset($u); $editQualities = Database::fetchAll("SELECT * FROM product_qualities WHERE product_id = :pid ORDER BY display_order ASC, id ASC", [':pid'=>$editId]); foreach ($editQualities as &$q) { $q['quality_name'] = _safeDec($q['quality_name'] ?? ''); $q['base_price'] = (float)(_safeDec($q['base_price'] ?? '') ?: $q['base_price']); } unset($q); $editTypes = Database::fetchAll("SELECT * FROM product_types WHERE product_id = :pid ORDER BY display_order ASC, id ASC", [':pid'=>$editId]); foreach ($editTypes as &$t) { $t['type_name'] = _safeDec($t['type_name'] ?? ''); $t['base_price'] = (float)(_safeDec($t['base_price'] ?? '') ?: $t['base_price']); } unset($t); } } ?> <style> .pm-toolbar { display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid rgba(255,255,255,.06); } .pm-btn-new { display:inline-flex;align-items:center;gap:8px;padding:10px 20px;border-radius:8px;background:rgba(45,106,79,.2);color:#4ade80;border:1px solid rgba(45,106,79,.35);font-size:12px;font-weight:700;letter-spacing:.5px;text-decoration:none;transition:background .2s; } .pm-btn-new:hover { background:rgba(45,106,79,.35); } .pm-count { font-size:12px;color:rgba(200,204,212,.4); } .pm-grid { display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px; } .pm-card { background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:12px;overflow:hidden;transition:border-color .2s; } .pm-card:hover { border-color:rgba(255,255,255,.18); } .pm-card-img { width:100%;height:180px;object-fit:cover;background:rgba(255,255,255,.02); } .pm-card-img-placeholder { width:100%;height:180px;background:linear-gradient(135deg,rgba(255,255,255,.02),rgba(255,255,255,.05));display:flex;align-items:center;justify-content:center;color:rgba(200,204,212,.2);font-size:28px; } .pm-card-body { padding:16px; } .pm-card-name { font-size:14px;font-weight:700;color:#e2e6ed;margin-bottom:6px; } .pm-card-price { font-size:13px;font-weight:600;color:#4ade80;margin-bottom:8px; } .pm-card-quality { font-size:11px;color:rgba(200,204,212,.4);margin-bottom:12px; } .pm-card-status { display:inline-block;font-size:10px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;padding:3px 8px;border-radius:4px; } .pm-card-status.active { background:rgba(74,222,128,.12);color:#4ade80; } .pm-card-status.draft { background:rgba(251,191,36,.12);color:#fbbf24; } .pm-card-status.archived { background:rgba(200,204,212,.08);color:rgba(200,204,212,.4); } .pm-card-actions { display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.06); } .pm-card-btn { flex:1;padding:7px;border-radius:6px;font-size:11px;font-weight:600;text-align:center;text-decoration:none;transition:all .2s;border:1px solid rgba(255,255,255,.08);color:rgba(200,204,212,.6);background:transparent;cursor:pointer;font-family:inherit; } .pm-card-btn:hover { border-color:rgba(255,255,255,.2);color:#e2e6ed; } .pm-card-btn.danger:hover { border-color:rgba(239,68,68,.4);color:#ef4444;background:rgba(239,68,68,.08); } .pm-form { max-width:700px; } .pm-form-group { margin-bottom:20px; } .pm-form-label { display:block;font-size:10px;font-weight:700;letter-spacing:.8px;text-transform:uppercase;color:rgba(200,204,212,.4);margin-bottom:6px; } .pm-form-input { width:100%;box-sizing:border-box;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.09);border-radius:8px;color:#e2e6ed;padding:10px 13px;font-size:13px;outline:none;transition:border-color .2s;font-family:inherit; } .pm-form-input:focus { border-color:rgba(255,255,255,.22); } textarea.pm-form-input { resize:vertical;min-height:80px; } select.pm-form-input { cursor:pointer; } .pm-form-file { padding:8px; } .pm-form-preview { width:120px;height:120px;border-radius:8px;object-fit:cover;margin-top:8px;border:1px solid rgba(255,255,255,.1); } .pm-form-row { display:flex;gap:12px;align-items:flex-end; } .pm-tiers { display:flex;flex-direction:column;gap:8px;margin-top:8px; } .pm-tier-row { display:flex;gap:8px;align-items:center; } .pm-tier-row input { flex:1; } .pm-tier-remove { width:32px;height:32px;border-radius:6px;border:1px solid rgba(239,68,68,.2);background:transparent;color:rgba(239,68,68,.6);font-size:16px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center; } .pm-tier-remove:hover { background:rgba(239,68,68,.1);color:#ef4444; } .pm-tier-add { display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:6px;border:1px solid rgba(74,222,128,.2);background:rgba(74,222,128,.06);color:#4ade80;font-size:11px;font-weight:700;cursor:pointer;transition:all .2s;font-family:inherit;margin-top:4px; } .pm-tier-add:hover { background:rgba(74,222,128,.15); } .pm-form-actions { display:flex;gap:12px;padding-top:20px;border-top:1px solid rgba(255,255,255,.06); } .pm-btn-save { padding:11px 28px;border-radius:8px;background:rgba(74,222,128,.15);color:#4ade80;border:1px solid rgba(74,222,128,.3);font-size:12px;font-weight:700;letter-spacing:.8px;cursor:pointer;transition:all .2s;font-family:inherit; } .pm-btn-save:hover { background:rgba(74,222,128,.25); } .pm-btn-cancel { padding:11px 20px;border-radius:8px;background:transparent;color:rgba(200,204,212,.4);border:1px solid rgba(255,255,255,.08);font-size:12px;font-weight:700;text-decoration:none;transition:all .2s; } .pm-btn-cancel:hover { color:#e2e6ed;border-color:rgba(255,255,255,.2); } .pm-toast { padding:10px 16px;border-radius:8px;font-size:12px;font-weight:600;margin-bottom:16px; } .pm-toast-ok { background:rgba(74,222,128,.1);color:#4ade80;border:1px solid rgba(74,222,128,.2); } .pm-toast-del { background:rgba(239,68,68,.1);color:#ef4444;border:1px solid rgba(239,68,68,.2); } .prd-group { background:rgba(255,255,255,.025); border:1px solid rgba(255,255,255,.06); border-radius:14px; padding:20px; margin-bottom:16px; } .prd-group-title { font-size:10px; font-weight:700; letter-spacing:1.2px; text-transform:uppercase; color:rgba(200,204,212,.3); margin-bottom:14px; padding-bottom:10px; border-bottom:1px solid rgba(255,255,255,.04); } .variant-row { display:grid; grid-template-columns:30px 1.5fr 1fr 1.5fr 1fr 40px; gap:12px; align-items:center; background:rgba(255,255,255,.015); border:1px solid rgba(255,255,255,.05); border-radius:10px; padding:8px 12px; margin-bottom:8px; } .variant-row-unit { display:grid; grid-template-columns:1.5fr 1fr 1fr 40px; gap:12px; align-items:center; background:rgba(255,255,255,.015); border:1px solid rgba(255,255,255,.05); border-radius:10px; padding:8px 12px; margin-bottom:8px; } .variant-row input.pm-form-input { padding:8px 10px; font-size:12px; } .variant-row-unit input.pm-form-input { padding:8px 10px; font-size:12px; } .var-file-label { padding:6px 10px; background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08); border-radius:6px; font-size:11px; cursor:pointer; color:#e2e6ed; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:140px; display:inline-block; text-align:center; width:100%; box-sizing:border-box; transition:all .2s; } .var-file-label:hover { border-color:rgba(74,222,128,.3); background:rgba(74,222,128,.04); } .var-img-preview { width:28px; height:28px; border-radius:4px; object-fit:cover; border:1px solid rgba(255,255,255,.1); } .enc-field { position:relative; } .enc-mask { font-family:'JetBrains Mono',monospace;font-size:11px;color:rgba(200,204,212,.25);letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px;display:inline-block;vertical-align:middle; } .enc-lock-btn { display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:5px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:rgba(200,204,212,.4);font-size:9px;font-weight:800;letter-spacing:.5px;cursor:pointer;transition:all .2s;font-family:inherit;vertical-align:middle;margin-left:6px; } .enc-lock-btn:hover { border-color:rgba(74,222,128,.3);color:#4ade80;background:rgba(74,222,128,.06); } .enc-lock-btn.unlocked { border-color:rgba(74,222,128,.25);color:#4ade80;background:rgba(74,222,128,.08); } .enc-plain { display:none; } .enc-field.revealed .enc-mask { display:none; } .enc-field.revealed .enc-plain { display:inline; } .enc-field.revealed .enc-lock-btn { border-color:rgba(239,68,68,.2);color:rgba(239,68,68,.6); } .enc-field.revealed .enc-lock-btn:hover { background:rgba(239,68,68,.08);color:#ef4444; } .pm-card-enc { font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(200,204,212,.2);letter-spacing:.3px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } .pm-card-reveal { display:none;font-size:14px;font-weight:700;color:#e2e6ed; } .pm-card-reveal-price { display:none;font-size:13px;font-weight:600;color:#4ade80; } .pm-card.unlocked .pm-card-enc { display:none; } .pm-card.unlocked .pm-card-reveal { display:block; } .pm-card.unlocked .pm-card-reveal-price { display:block; } .pm-card.unlocked .pm-card-name { display:none; } .pm-card.unlocked .pm-card-price { display:none; } .pm-card-unlock { padding:4px 10px;border-radius:5px;border:1px solid rgba(255,255,255,.06);background:transparent;color:rgba(200,204,212,.3);font-size:9px;font-weight:800;cursor:pointer;transition:all .2s;font-family:inherit;letter-spacing:.5px; } .pm-card-unlock:hover { border-color:rgba(74,222,128,.3);color:#4ade80; } .pm-card-unlock.on { border-color:rgba(74,222,128,.25);color:#4ade80;background:rgba(74,222,128,.06); } .pm-form-input.locked { background:rgba(255,255,255,.02);border-color:rgba(255,255,255,.04);color:transparent;text-shadow:0 0 8px rgba(200,204,212,.3);pointer-events:none;user-select:none; } .pm-form-lock-wrap { position:relative; } .pm-form-lock-btn { position:absolute;right:8px;top:50%;transform:translateY(-50%);padding:4px 10px;border-radius:5px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.03);color:rgba(200,204,212,.4);font-size:9px;font-weight:800;letter-spacing:.5px;cursor:pointer;transition:all .2s;font-family:inherit;z-index:2; } .pm-form-lock-btn:hover { border-color:rgba(74,222,128,.3);color:#4ade80;background:rgba(74,222,128,.06); } .pm-form-lock-btn.unlocked { border-color:rgba(74,222,128,.25);color:#4ade80;background:rgba(74,222,128,.08); } </style><?php if (isset($_GET['saved'])): ?> <div class="pm-toast pm-toast-ok">✓ Product saved successfully.</div><?php elseif (isset($_GET['deleted'])): ?> <div class="pm-toast pm-toast-del">✓ Product deleted.</div><?php endif; ?> <?php if ($action === 'new' || ($action === 'edit' && $editProduct)): ?> <div class="pm-form"> <form method="POST" enctype="multipart/form-data" action="?view=store&id=<?= $editorStoreId ?>&action=products"><?= CSRF::field() ?> <input type="hidden" name="store" value="<?= $editorStoreId ?>"> <input type="hidden" name="action" value="save"> <input type="hidden" name="product_id" value="<?= $editProduct['id'] ?? 0 ?> "> <input type="hidden" name="existing_image" value="<?= UI::e($editProduct['image_path'] ?? '') ?> "> <div class="pm-form-group"> <label class="pm-form-label">Product Name <span class="cfg-enc">ENC</span></label> <div class="pm-form-lock-wrap"> <input class="pm-form-input<?= $editProduct ? ' locked' : '' ?>" type="text" name="name" value="<?= UI::e($editProduct['_name'] ?? '') ?>" required placeholder="e.g. Premium Extract" <?= $editProduct ? 'readonly' : '' ?> data-enc-field> <?php if ($editProduct): ?><button type="button" class="pm-form-lock-btn" data-action="toggleFieldLock">🔒 UNLOCK</button><?php endif; ?> </div> <?php if ($editProduct): ?><div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:rgba(200,204,212,.15);margin-top:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?= UI::e(substr($editProduct['name'] ?? '', 0, 60)) ?>...</div><?php endif; ?> </div> <div class="pm-form-group"> <label class="pm-form-label">Product Image</label> <input class="pm-form-input pm-form-file" type="file" name="image" accept="image/jpeg,image/png,image/webp"><?php if (!empty($editProduct['image_path'])): ?> <img class="pm-form-preview" src="/<?= UI::e($editProduct['image_path']) ?> " alt="Preview"><?php endif; ?> </div> <div class="pm-form-row"> <div class="pm-form-group" style="flex:1"> <label class="pm-form-label">Price Per 1 Unit ($) <span class="cfg-enc">ENC</span></label> <div class="pm-form-lock-wrap"> <input class="pm-form-input<?= $editProduct ? ' locked' : '' ?>" type="text" name="base_price" value="<?= UI::e($editProduct['_price'] ?? '') ?>" required placeholder="49.99" <?= $editProduct ? 'readonly' : '' ?> data-enc-field> <?php if ($editProduct): ?><button type="button" class="pm-form-lock-btn" data-action="toggleFieldLock">🔒 UNLOCK</button><?php endif; ?> </div> </div> <div class="pm-form-group" style="flex:1"> <label class="pm-form-label" style="color:#4ade80;font-weight:700">STATUS</label> <select class="pm-form-input" name="status" style="font-weight:700;font-size:14px;border:2px solid rgba(74,222,128,0.3);background:rgba(74,222,128,0.05)"> <option value="draft"<?= ($editProduct['status'] ?? '') === 'draft' ? ' selected' : '' ?> style="color:#f59e0b">● Draft</option> <option value="active"<?= ($editProduct['status'] ?? '') === 'active' ? ' selected' : '' ?> style="color:#4ade80">● Live</option> <option value="archived"<?= ($editProduct['status'] ?? '') === 'archived' ? ' selected' : '' ?> style="color:#ef4444">● Archived</option> </select> </div> <div class="pm-form-group" style="width:100px"> <label class="pm-form-label">Order</label> <input class="pm-form-input" type="number" name="display_order" value="<?= (int)($editProduct['display_order'] ?? 0) ?> " min="0"> </div> </div> <div class="prd-group">   <div class="prd-group-title"><?= Icons::types(16) ?> PRODUCT TYPES</div>   <div style="font-size:10px;color:rgba(200,204,212,.4);margin-bottom:12px;">Add product types. Each type can have its own price and image.</div>   <div id="types-container">     <?php foreach ($editTypes as $idx => $t): ?>       <div class="variant-row" data-index="<?= $idx ?>">         <input type="hidden" name="types[<?= $idx ?>][id]" value="<?= $t['id'] ?>">         <div>           <input type="checkbox" name="types[<?= $idx ?>][active]" value="1" <?= $t['is_active'] ? 'checked' : '' ?> style="width:16px;height:16px;accent-color:#4ade80">         </div>         <div>           <input class="pm-form-input" type="text" name="types[<?= $idx ?>][name]" value="<?= htmlspecialchars($t['type_name']) ?>" required placeholder="Type Name">         </div>         <div>           <input class="pm-form-input" type="number" step="0.01" name="types[<?= $idx ?>][price]" value="<?= $t['base_price'] ?>" required placeholder="Price">         </div>         <div style="display:flex;align-items:center;gap:8px">           <input type="file" id="type_img_<?= $idx ?>" name="type_image_<?= $idx ?>" style="display:none" accept="image/*" class="var-file-input">           <label for="type_img_<?= $idx ?>" class="var-file-label"><?= $t['image_path'] ? '📎 ' . basename($t['image_path']) : '📷 Upload' ?></label>           <?php if ($t['image_path']): ?>             <img class="var-img-preview" src="<?= htmlspecialchars($t['image_path']) ?>" alt="">           <?php endif; ?>         </div>         <div>           <input class="pm-form-input" type="number" name="types[<?= $idx ?>][display_order]" value="<?= $t['display_order'] ?>" style="text-align:center">         </div>         <div>           <button type="button" class="pm-tier-remove var-del-btn" style="padding:6px 10px;font-size:12px">×</button>         </div>       </div>     <?php endforeach; ?>   </div>   <button type="button" class="pm-tier-add" id="btn-add-type" style="padding:8px 16px;font-size:11px">+ ADD TYPE</button> </div>  <div class="prd-group">   <div class="prd-group-title"><?= Icons::quality(16) ?> QUALITY TIERS</div>   <div style="font-size:10px;color:rgba(200,204,212,.4);margin-bottom:12px;">Add quality tiers. Each quality can have its own price and image.</div>   <div id="qualities-container">     <?php foreach ($editQualities as $idx => $q): ?>       <div class="variant-row" data-index="<?= $idx ?>">         <input type="hidden" name="qualities[<?= $idx ?>][id]" value="<?= $q['id'] ?>">         <div>           <input type="checkbox" name="qualities[<?= $idx ?>][active]" value="1" <?= $q['is_active'] ? 'checked' : '' ?> style="width:16px;height:16px;accent-color:#4ade80">         </div>         <div>           <input class="pm-form-input" type="text" name="qualities[<?= $idx ?>][name]" value="<?= htmlspecialchars($q['quality_name']) ?>" required placeholder="Quality Name">         </div>         <div>           <input class="pm-form-input" type="number" step="0.01" name="qualities[<?= $idx ?>][price]" value="<?= $q['base_price'] ?>" required placeholder="Price">         </div>         <div style="display:flex;align-items:center;gap:8px">           <input type="file" id="qual_img_<?= $idx ?>" name="quality_image_<?= $idx ?>" style="display:none" accept="image/*" class="var-file-input">           <label for="qual_img_<?= $idx ?>" class="var-file-label"><?= $q['image_path'] ? '📎 ' . basename($q['image_path']) : '📷 Upload' ?></label>           <?php if ($q['image_path']): ?>             <img class="var-img-preview" src="<?= htmlspecialchars($q['image_path']) ?>" alt="">           <?php endif; ?>         </div>         <div>           <input class="pm-form-input" type="number" name="qualities[<?= $idx ?>][display_order]" value="<?= $q['display_order'] ?>" style="text-align:center">         </div>         <div>           <button type="button" class="pm-tier-remove var-del-btn" style="padding:6px 10px;font-size:12px">×</button>         </div>       </div>     <?php endforeach; ?>   </div>   <button type="button" class="pm-tier-add" id="btn-add-quality" style="padding:8px 16px;font-size:11px">+ ADD QUALITY</button> </div>  <div class="prd-group">   <div class="prd-group-title"><?= Icons::sizing(16) ?> SIZING / UNIT PRICING</div>   <div style="font-size:10px;color:rgba(200,204,212,.4);margin-bottom:12px;">Sizing multipliers (e.g. 1G = 1.0, 2G = 2.0).</div>   <div id="units-container">     <?php foreach ($editUnits as $idx => $u): ?>       <div class="variant-row-unit" data-index="<?= $idx ?>">         <input type="hidden" name="units[<?= $idx ?>][id]" value="<?= $u['id'] ?>">         <div>           <input class="pm-form-input" type="text" name="units[<?= $idx ?>][amount]" value="<?= htmlspecialchars($u['unit_amount']) ?>" required placeholder="Size (e.g. 1G)">         </div>         <div>           <input class="pm-form-input" type="number" step="0.01" name="units[<?= $idx ?>][price]" value="<?= $u['unit_price'] ?>" required placeholder="Multiplier (e.g. 1.0)">         </div>         <div>           <input class="pm-form-input" type="number" name="units[<?= $idx ?>][display_order]" value="<?= $u['display_order'] ?>" style="text-align:center">         </div>         <div>           <button type="button" class="pm-tier-remove var-del-btn" style="padding:6px 10px;font-size:12px">×</button>         </div>       </div>     <?php endforeach; ?>   </div>   <button type="button" class="pm-tier-add" id="btn-add-unit" style="padding:8px 16px;font-size:11px">+ ADD SIZE TIER</button> </div>  <div class="pm-form-group"> <label class="pm-form-label">Notes / Description <span class="cfg-enc">ENC</span></label> <div class="pm-form-lock-wrap"> <textarea class="pm-form-input<?= $editProduct ? ' locked' : '' ?>" name="notes" placeholder="Brief product write-up..." <?= $editProduct ? 'readonly' : '' ?> data-enc-field><?= UI::e($editProduct['_notes'] ?? '') ?></textarea> <?php if ($editProduct): ?><button type="button" class="pm-form-lock-btn" data-action="toggleFieldLock" style="top:20px;transform:none">🔒 UNLOCK</button><?php endif; ?> </div> </div>  <div class="pm-form-actions"> <button type="submit" class="pm-btn-save">🔐 SAVE & RE-ENCRYPT</button> <a href="?view=store&id=<?= $editorStoreId ?>&action=products" class="pm-btn-cancel">Cancel</a> </div> </form> </div> <script nonce="<?= $cspNonce ?>"> (function() { 	function updateVariantFileLabel(input) { 		if (input.files && input.files[0]) { 			const label = input.nextElementSibling; 			if (label) { 				label.textContent = '📎 ' + input.files[0].name.substring(0, 15); 			} 		} 	}  	let typeIndex = <?= count($editTypes) ?>; 	function addTypeRow() { 		const container = document.getElementById('types-container'); 		if (!container) return; 		const html = ` 			<div class="variant-row" data-index="${typeIndex}"> 				<input type="hidden" name="types[${typeIndex}][id]" value=""> 				<div> 					<input type="checkbox" name="types[${typeIndex}][active]" value="1" checked style="width:16px;height:16px;accent-color:#4ade80"> 				</div> 				<div> 					<input class="pm-form-input" type="text" name="types[${typeIndex}][name]" required placeholder="Type Name"> 				</div> 				<div> 					<input class="pm-form-input" type="number" step="0.01" name="types[${typeIndex}][price]" required placeholder="Price"> 				</div> 				<div style="display:flex;align-items:center;gap:8px"> 					<input type="file" id="type_img_${typeIndex}" name="type_image_${typeIndex}" style="display:none" accept="image/*" class="var-file-input"> 					<label for="type_img_${typeIndex}" class="var-file-label">📷 Upload</label> 				</div> 				<div> 					<input class="pm-form-input" type="number" name="types[${typeIndex}][display_order]" value="${typeIndex}" style="text-align:center"> 				</div> 				<div> 					<button type="button" class="pm-tier-remove var-del-btn" style="padding:6px 10px;font-size:12px">×</button> 				</div> 			</div> 		`; 		container.insertAdjacentHTML('beforeend', html); 		typeIndex++; 	}  	let qualIndex = <?= count($editQualities) ?>; 	function addQualityRow() { 		const container = document.getElementById('qualities-container'); 		if (!container) return; 		const html = ` 			<div class="variant-row" data-index="${qualIndex}"> 				<input type="hidden" name="qualities[${qualIndex}][id]" value=""> 				<div> 					<input type="checkbox" name="qualities[${qualIndex}][active]" value="1" checked style="width:16px;height:16px;accent-color:#4ade80"> 				</div> 				<div> 					<input class="pm-form-input" type="text" name="qualities[${qualIndex}][name]" required placeholder="Quality Name"> 				</div> 				<div> 					<input class="pm-form-input" type="number" step="0.01" name="qualities[${qualIndex}][price]" required placeholder="Price"> 				</div> 				<div style="display:flex;align-items:center;gap:8px"> 					<input type="file" id="qual_img_${qualIndex}" name="quality_image_${qualIndex}" style="display:none" accept="image/*" class="var-file-input"> 					<label for="qual_img_${qualIndex}" class="var-file-label">📷 Upload</label> 				</div> 				<div> 					<input class="pm-form-input" type="number" name="qualities[${qualIndex}][display_order]" value="${qualIndex}" style="text-align:center"> 				</div> 				<div> 					<button type="button" class="pm-tier-remove var-del-btn" style="padding:6px 10px;font-size:12px">×</button> 				</div> 			</div> 		`; 		container.insertAdjacentHTML('beforeend', html); 		qualIndex++; 	}  	let unitIndex = <?= count($editUnits) ?>; 	function addUnitRow() { 		const container = document.getElementById('units-container'); 		if (!container) return; 		const html = ` 			<div class="variant-row-unit" data-index="${unitIndex}"> 				<input type="hidden" name="units[${unitIndex}][id]" value=""> 				<div> 					<input class="pm-form-input" type="text" name="units[${unitIndex}][amount]" required placeholder="Size (e.g. 1G)"> 				</div> 				<div> 					<input class="pm-form-input" type="number" step="0.01" name="units[${unitIndex}][price]" required placeholder="Multiplier (e.g. 1.0)"> 				</div> 				<div> 					<input class="pm-form-input" type="number" name="units[${unitIndex}][display_order]" value="${unitIndex}" style="text-align:center"> 				</div> 				<div> 					<button type="button" class="pm-tier-remove var-del-btn" style="padding:6px 10px;font-size:12px">×</button> 				</div> 			</div> 		`; 		container.insertAdjacentHTML('beforeend', html); 		unitIndex++; 	}  	function bindEvents() { 		const addTypeBtn = document.getElementById('btn-add-type'); 		if (addTypeBtn) addTypeBtn.addEventListener('click', addTypeRow); 		const addQualBtn = document.getElementById('btn-add-quality'); 		if (addQualBtn) addQualBtn.addEventListener('click', addQualityRow); 		const addUnitBtn = document.getElementById('btn-add-unit'); 		if (addUnitBtn) addUnitBtn.addEventListener('click', addUnitRow); 	}  	document.addEventListener('DOMContentLoaded', bindEvents); 	bindEvents();  	document.addEventListener('click', function(e) { 		if (e.target && e.target.classList.contains('var-del-btn')) { 			e.preventDefault(); 			const row = e.target.closest('.variant-row') || e.target.closest('.variant-row-unit'); 			if (row) row.remove(); 		} 	});  	document.addEventListener('change', function(e) { 		if (e.target && e.target.classList.contains('var-file-input')) { 			updateVariantFileLabel(e.target); 		} 	}); })();  document.addEventListener('click', function(e) {     var btn = e.target.closest('[data-action="toggleFieldLock"]');     if (btn) toggleFieldLock(btn);     var cardBtn = e.target.closest('[data-action="toggleCardLock"]');     if (cardBtn) toggleCardLock(cardBtn); });  document.addEventListener('change', function(e) {     var sel = e.target.closest('[data-action="storeSelectChange"]');     if (sel) location.href = sel.dataset.prefix + sel.value + sel.dataset.suffix; });  document.addEventListener('submit', function(e) {     var form = e.target.closest('[data-action="confirmSubmit"]');     if (form) {         if (!confirm(form.dataset.msg)) {             e.preventDefault();         }     } }); </script><?php else: ?> <div class="pm-toolbar"> 	<div style="display:flex; align-items:center; gap:12px;"> 		<a href="?view=store&id=<?= $editorStoreId ?>&action=new_product" class="pm-btn-new"> 			<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> 			NEW PRODUCT 		</a> 		<select class="pm-form-input" style="width:auto; margin:0; height:38px; padding:0 10px;" id="pm-store-select" data-action="storeSelectChange" data-prefix="?view=store&id=" data-suffix="&action=products"> 			<?php foreach ($stores as $s): ?> 				<option value="<?= (int)$s['id'] ?>"<?= (int)$s['id'] === $editorStoreId ? ' selected' : '' ?>> 					<?= htmlspecialchars($s['nickname'] ?: $s['domain']) ?> (<?= htmlspecialchars($s['domain']) ?>) 				</option> 			<?php endforeach; ?> 		</select> 	</div> 	<span class="pm-count"><?= count($products) ?> product<?= count($products) !== 1 ? 's' : '' ?></span> </div><?php if (empty($products)): ?> <div style="text-align:center;padding:60px 0;color:rgba(200,204,212,.3);font-size:13px">No products yet. Click <strong>NEW PRODUCT</strong> to create one.</div><?php else: ?> <div class="pm-grid"><?php foreach ($products as $p): ?> <div class="pm-card" data-locked="1"> <div style="position:relative"><?php if (!empty($p['image_path'])): ?> <img class="pm-card-img" src="/<?= UI::e($p['image_path']) ?>" alt=""><?php else: ?> <div class="pm-card-img-placeholder">📦</div><?php endif; ?> <button type="button" class="pm-card-unlock" data-action="toggleCardLock" style="position:absolute;top:8px;right:8px">🔒 LOCKED</button> </div> <div class="pm-card-body"> <div class="pm-card-sku" style="font-size:14px;font-weight:800;font-family:monospace;color:#4ade80;letter-spacing:1px;margin-bottom:8px;border-bottom:1px solid rgba(255,255,255,.06);padding-bottom:8px"> <?= UI::e($p['sku'] ?? '—') ?> </div> <div class="pm-card-enc"><?= UI::e($p['_enc_name']) ?>...</div> <div class="pm-card-reveal" style="display:none"><?= UI::e($p['_name']) ?></div> <div class="pm-card-name" style="display:none"><?= UI::e($p['_name']) ?></div> <div class="pm-card-enc" style="margin-top:4px">$<?= UI::e($p['_enc_price']) ?></div> <div class="pm-card-reveal-price" style="display:none">$<?= UI::e($p['_price']) ?></div> <div class="pm-card-price" style="display:none">$<?= UI::e($p['_price']) ?></div> <div class="pm-card-quality"><?= UI::e($p['_quality']) ?></div> <span class="pm-card-status<?= $p['status'] ?>"><?= $p['status'] ?></span> <div class="pm-card-actions"> 	<a href="?view=store&id=<?= $editorStoreId ?>&action=edit_product&pid=<?= (int)$p['id'] ?>" class="pm-card-btn">🔓 Edit</a> 	<form method="POST" action="?view=store&id=<?= $editorStoreId ?>&action=products" style="flex:1;display:flex" data-action="confirmSubmit" data-msg="Delete this product?"><?= CSRF::field() ?> 		<input type="hidden" name="store" value="<?= $editorStoreId ?>"> 		<input type="hidden" name="action" value="delete"> 		<input type="hidden" name="delete_id" value="<?= (int)$p['id'] ?>"> 		<button type="submit" class="pm-card-btn danger" style="width:100%">Delete</button> 	</form> </div> </div> </div><?php endforeach; ?> </div><?php endif; ?> <?php endif; ?> <script nonce="<?= $cspNonce ?>"> function toggleCardLock(btn) {     const card = btn.closest('.pm-card');     const isLocked = card.dataset.locked === '1';     card.querySelectorAll('.pm-card-enc').forEach(el => el.style.display = isLocked ? 'none' : 'block');     card.querySelectorAll('.pm-card-reveal, .pm-card-reveal-price, .pm-card-name, .pm-card-price').forEach(el => el.style.display = isLocked ? 'block' : 'none');     btn.textContent = isLocked ? '🔓 UNLOCKED' : '🔒 LOCKED';     card.dataset.locked = isLocked ? '0' : '1'; } function toggleFieldLock(btn) {     var wrap = btn.closest('.pm-form-lock-wrap');     if (!wrap) return;     var inp = wrap.querySelector('input[data-enc-field], textarea[data-enc-field]');     if (!inp) return;     var locked = inp.classList.contains('locked');     inp.classList.toggle('locked', !locked);     inp.readOnly = !locked;     btn.textContent = locked ? '🔒 UNLOCK' : '🔓 LOCK';     btn.classList.toggle('unlocked', locked); }  document.addEventListener('click', function(e) {     var btn = e.target.closest('[data-action="toggleFieldLock"]');     if (btn) toggleFieldLock(btn);     var cardBtn = e.target.closest('[data-action="toggleCardLock"]');     if (cardBtn) toggleCardLock(cardBtn); });  document.addEventListener('change', function(e) {     var sel = e.target.closest('[data-action="storeSelectChange"]');     if (sel) location.href = sel.dataset.prefix + sel.value + sel.dataset.suffix; });  document.addEventListener('submit', function(e) {     var form = e.target.closest('[data-action="confirmSubmit"]');     if (form) {         if (!confirm(form.dataset.msg)) {             e.preventDefault();         }     } }); </script>
