PDO::ERRMODE_EXCEPTION ]); $pdo->exec("CREATE DATABASE IF NOT EXISTS `$dbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); $pdo->exec("USE `$dbName`"); $pdo->exec(" CREATE TABLE IF NOT EXISTS `products` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `codigo_producto` VARCHAR(12) NOT NULL UNIQUE, `codigo_secundario` VARCHAR(12) NOT NULL UNIQUE, `descripcion` VARCHAR(255) NOT NULL, `ingredientes` TEXT, `sabor` VARCHAR(100), `nombre_comercial` VARCHAR(150), `fecha_vencimiento` DATE, `rubro` VARCHAR(100), `tipo_producto` VARCHAR(100), `proveedor` VARCHAR(150), `sucursal` VARCHAR(100), `ubicacion_almacen` VARCHAR(100), `peso_concentracion` DECIMAL(10,3), `unidad_concentracion` VARCHAR(50), `presentacion_qty` INT, `unidad_presentacion` VARCHAR(50), `stock_minimo` INT, `stock_actual` INT, `precio_compra` DECIMAL(12,2), `ganancia` DECIMAL(12,2), `porcentaje_descuento` DECIMAL(5,2) DEFAULT 0, `precio_venta` DECIMAL(12,2), `igv` DECIMAL(5,2) DEFAULT $defaultIGV, `imagen_path` VARCHAR(255), `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY (`descripcion`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); // intentar crear indice único para nombre_comercial (si no existe) try { $pdo->exec("ALTER TABLE products ADD UNIQUE INDEX uniq_nombre_comercial (nombre_comercial)"); } catch (Exception $e) { // ignorar si ya existe (o si nombre_comercial tiene duplicados) } } catch (Exception $e) { die("Error de conexión / creación DB: " . htmlspecialchars($e->getMessage())); } /* Asegurar carpeta images */ if (!file_exists($imagesFolder)) mkdir($imagesFolder, 0755, true); /* Helper: generar siguiente código */ function next_code($pdo, $prefix, $digits = 4) { $stmt = $pdo->prepare("SELECT codigo_producto, codigo_secundario FROM products WHERE codigo_producto LIKE :like OR codigo_secundario LIKE :like ORDER BY id DESC LIMIT 1"); $stmt->execute([':like' => $prefix . '%']); $row = $stmt->fetch(PDO::FETCH_ASSOC); $lastNum = 0; if ($row) { foreach (['codigo_producto','codigo_secundario'] as $col) { if (!empty($row[$col]) && strpos($row[$col], $prefix) === 0) { $numPart = preg_replace('/[^0-9]/', '', $row[$col]); if (is_numeric($numPart)) $lastNum = max($lastNum, intval($numPart)); } } } $next = $lastNum + 1; return $prefix . str_pad($next, $digits, '0', STR_PAD_LEFT); } /* Valores iniciales para mostrar */ $nextAJ = next_code($pdo, 'AJ', 4); $nextASR = next_code($pdo, 'ASR', 4); /* Buscar logo: images/logo_artika.png preferido */ $logoPath = null; if (file_exists($imagesFolder . '/logo_artika.png')) { $logoPath = 'images/logo_artika.png'; } else { foreach (['logo.png','logo.jpg','logo.jpeg','logo.svg'] as $f) { if (file_exists($imagesFolder . '/' . $f)) { $logoPath = 'images/' . $f; break; } } } /* Opciones default para selects */ $rubro_options = ['Helados','Congeladora','Carreta de helados','Postres','Bebidas']; $tipo_options = ['Paleta','Vaso','Sándwich','Rellenas','Torta']; $proveedor_options = ['Inversiones Artika Lima','Inversiones Artika Cusco','Proveedor Local']; $sucursal_options = ['Juliaca','Puno','Cabanilla']; $ubicacion_options = ['Tienda','Almacén','Refrigerador','Congelador Central']; $unidad_presentacion_options = ['paletas/caja','vasos/caja','unidad','kg','litro']; $unidad_medida_options = ['gr','kg','ml','lt','oz','Otro']; /* Mensajes desde sesión (PRG) */ $successMsg = $_SESSION['success_msg'] ?? null; $warningsFromSession = $_SESSION['warnings'] ?? []; unset($_SESSION['success_msg'], $_SESSION['warnings']); /* POST handling */ $errors = []; $warnings = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // leer campos $codigo_producto = $_POST['codigo_producto'] ?? next_code($pdo,'AJ',4); $codigo_secundario = $_POST['codigo_secundario'] ?? next_code($pdo,'ASR',4); $descripcion = trim($_POST['descripcion'] ?? ''); $ingredientes = trim($_POST['ingredientes'] ?? ''); $sabor = trim($_POST['sabor'] ?? ''); if ($sabor === 'Otro') $sabor = trim($_POST['sabor_otro'] ?? ''); $nombre_comercial = trim($_POST['nombre_comercial'] ?? ''); $fecha_vencimiento = $_POST['fecha_vencimiento'] ?? null; $rubro = ($_POST['rubro_select'] ?? '') === 'Otro' ? trim($_POST['rubro_otro'] ?? '') : ($_POST['rubro_select'] ?? ''); $tipo_producto = ($_POST['tipo_select'] ?? '') === 'Otro' ? trim($_POST['tipo_otro'] ?? '') : ($_POST['tipo_select'] ?? ''); $proveedor = ($_POST['proveedor_select'] ?? '') === 'Otro' ? trim($_POST['proveedor_otro'] ?? '') : ($_POST['proveedor_select'] ?? ''); $sucursal = ($_POST['sucursal_select'] ?? '') === 'Otro' ? trim($_POST['sucursal_otro'] ?? '') : ($_POST['sucursal_select'] ?? ''); $ubicacion_almacen = ($_POST['ubicacion_select'] ?? '') === 'Otro' ? trim($_POST['ubicacion_otro'] ?? '') : ($_POST['ubicacion_select'] ?? ''); $peso_concentracion = $_POST['peso_concentracion'] ?? null; $unidad_concentracion = ($_POST['unidad_medida'] ?? ''); if ($unidad_concentracion === 'Otro') $unidad_concentracion = trim($_POST['unidad_medida_otro'] ?? ''); $presentacion_qty = $_POST['presentacion_qty'] ?? null; $unidad_presentacion = ($_POST['unidad_presentacion_select'] ?? '') === 'Otro' ? trim($_POST['unidad_presentacion_otro'] ?? '') : ($_POST['unidad_presentacion_select'] ?? ''); $stock_minimo = is_numeric($_POST['stock_minimo'] ?? null) ? intval($_POST['stock_minimo']) : 0; $stock_actual = is_numeric($_POST['stock_actual'] ?? null) ? intval($_POST['stock_actual']) : 0; $precio_compra = is_numeric($_POST['precio_compra'] ?? null) ? floatval($_POST['precio_compra']) : 0.0; $ganancia = is_numeric($_POST['ganancia'] ?? null) ? floatval($_POST['ganancia']) : 0.0; $porcentaje_descuento = is_numeric($_POST['porcentaje_descuento'] ?? null) ? floatval($_POST['porcentaje_descuento']) : 0.0; // precio_venta no confiaremos en POST, lo calcularemos en servidor para evitar manipulacion $igv = is_numeric($_POST['igv'] ?? null) ? floatval($_POST['igv']) : $defaultIGV; // validaciones if ($descripcion === '') $errors[] = "La descripción es obligatoria."; if ($stock_actual < 0) $errors[] = "Stock actual inválido."; if ($precio_compra < 0) $errors[] = "Precio de compra inválido."; if ($nombre_comercial === '') $errors[] = "El nombre comercial es obligatorio."; // duplicados: descripcion if (empty($errors)) { $stmt = $pdo->prepare("SELECT COUNT(*) FROM products WHERE LOWER(descripcion) = LOWER(:desc)"); $stmt->execute([':desc'=>$descripcion]); if ($stmt->fetchColumn() > 0) $errors[] = "Ya existe un producto con esa descripción."; } // duplicado: nombre_comercial if (empty($errors)) { $stmt = $pdo->prepare("SELECT COUNT(*) FROM products WHERE LOWER(nombre_comercial) = LOWER(:nc)"); $stmt->execute([':nc'=>$nombre_comercial]); if ($stmt->fetchColumn() > 0) $errors[] = "Ya existe un producto con ese Nombre Comercial."; } // calcular precio_venta en servidor (compra + ganancia - %descuento) $pv_base = $precio_compra + $ganancia; $computedPrecioVenta = $pv_base - ($pv_base * ($porcentaje_descuento / 100.0)); // asegurar dos decimales y no negativo $computedPrecioVenta = round(max(0, $computedPrecioVenta), 2); // manejar imagen (con fallback si GD no disponible) $imagen_path = null; if (isset($_FILES['imagen']) && $_FILES['imagen']['error'] !== UPLOAD_ERR_NO_FILE) { $file = $_FILES['imagen']; if ($file['error'] !== UPLOAD_ERR_OK) { $errors[] = "Error al subir la imagen (código de error {$file['error']})."; } elseif ($file['size'] > $maxFileSizeBytes) { $errors[] = "La imagen supera el tamaño permitido (4 MB)."; } else { // Detectar tipo con finfo si está $mime = null; if (function_exists('finfo_open')) { $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); } elseif (@getimagesize($file['tmp_name'])) { $info = getimagesize($file['tmp_name']); $mime = $info['mime'] ?? null; } else { $mime = mime_content_type($file['tmp_name'] ?? ''); } $ext = null; if ($mime === 'image/png') $ext = 'png'; elseif ($mime === 'image/jpeg' || $mime === 'image/jpg') $ext = 'jpg'; elseif ($mime === 'image/svg+xml') $ext = 'svg'; else { // fallback por extension del nombre $e = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (in_array($e, ['png','jpg','jpeg','svg'])) $ext = $e === 'jpeg' ? 'jpg' : $e; } if (!$ext) { $errors[] = "Tipo de imagen no soportado."; } else { $newFilename = $codigo_producto . '_' . time() . '.' . $ext; $destPath = $imagesFolder . '/' . $newFilename; // Detectar si GD está disponible y puede procesar jpg/png $gdAvailable = function_exists('imagecreatetruecolor') && (function_exists('imagecreatefromjpeg') || function_exists('imagecreatefrompng')); if (!$gdAvailable || $ext === 'svg') { // fallback: mover sin redimensionar if (!move_uploaded_file($file['tmp_name'], $destPath)) { $errors[] = "Error al mover la imagen al destino."; } else { $imagen_path = 'images/' . $newFilename; $warnings[] = "Imagen guardada sin redimensionar (GD no disponible o formato SVG). Recomiendo activar la extensión GD en PHP para redimensionado automático."; } } else { // intentamos redimensionar $info = @getimagesize($file['tmp_name']); if (!$info) { // fallback mover if (!move_uploaded_file($file['tmp_name'], $destPath)) $errors[] = "Error al mover la imagen."; else { $imagen_path = 'images/' . $newFilename; $warnings[] = "Imagen guardada sin redimensionar (getimagesize falló)."; } } else { $w = $info[0]; $h = $info[1]; $scale = min($maxImageDimension / $w, $maxImageDimension / $h, 1); $newW = (int)round($w * $scale); $newH = (int)round($h * $scale); if ($ext === 'png') { $src = @imagecreatefrompng($file['tmp_name']); if (!$src) { // fallback if (!move_uploaded_file($file['tmp_name'], $destPath)) $errors[] = "Error al mover PNG."; else { $imagen_path = 'images/' . $newFilename; $warnings[]="PNG guardado sin redimensionar."; } } else { $dst = imagecreatetruecolor($newW, $newH); imagealphablending($dst, false); imagesavealpha($dst, true); imagecopyresampled($dst, $src, 0,0,0,0, $newW,$newH, $w,$h); imagepng($dst, $destPath); imagedestroy($src); imagedestroy($dst); $imagen_path = 'images/' . $newFilename; } } else { // jpg if (!function_exists('imagecreatefromjpeg')) { // unexpected but check again if (!move_uploaded_file($file['tmp_name'], $destPath)) $errors[] = "Error al mover JPG (GD faltante)."; else { $imagen_path = 'images/' . $newFilename; $warnings[]="JPG guardado sin redimensionar (GD no disponible)."; } } else { $src = @imagecreatefromjpeg($file['tmp_name']); if (!$src) { if (!move_uploaded_file($file['tmp_name'], $destPath)) $errors[] = "Error al mover JPG."; else { $imagen_path = 'images/' . $newFilename; $warnings[]="JPG guardado sin redimensionar."; } } else { $dst = imagecreatetruecolor($newW, $newH); imagecopyresampled($dst, $src, 0,0,0,0, $newW,$newH, $w,$h); imagejpeg($dst, $destPath, 86); imagedestroy($src); imagedestroy($dst); $imagen_path = 'images/' . $newFilename; } } } } } } } } // insertar si no hay errores if (empty($errors)) { $insert = $pdo->prepare("INSERT INTO products (codigo_producto,codigo_secundario,descripcion,ingredientes,sabor,nombre_comercial,fecha_vencimiento,rubro,tipo_producto,proveedor,sucursal,ubicacion_almacen,peso_concentracion,unidad_concentracion,presentacion_qty,unidad_presentacion,stock_minimo,stock_actual,precio_compra,ganancia,porcentaje_descuento,precio_venta,igv,imagen_path) VALUES (:codigo_producto,:codigo_secundario,:descripcion,:ingredientes,:sabor,:nombre_comercial,:fecha_vencimiento,:rubro,:tipo_producto,:proveedor,:sucursal,:ubicacion_almacen,:peso_concentracion,:unidad_concentracion,:presentacion_qty,:unidad_presentacion,:stock_minimo,:stock_actual,:precio_compra,:ganancia,:porcentaje_descuento,:precio_venta,:igv,:imagen_path) "); $ok = $insert->execute([ ':codigo_producto'=>$codigo_producto, ':codigo_secundario'=>$codigo_secundario, ':descripcion'=>$descripcion, ':ingredientes'=>$ingredientes, ':sabor'=>$sabor, ':nombre_comercial'=>$nombre_comercial, ':fecha_vencimiento'=> $fecha_vencimiento ? $fecha_vencimiento : null, ':rubro'=>$rubro, ':tipo_producto'=>$tipo_producto, ':proveedor'=>$proveedor, ':sucursal'=>$sucursal, ':ubicacion_almacen'=>$ubicacion_almacen, ':peso_concentracion'=> $peso_concentracion ?: null, ':unidad_concentracion'=>$unidad_concentracion, ':presentacion_qty'=> $presentacion_qty ?: null, ':unidad_presentacion'=>$unidad_presentacion, ':stock_minimo'=> (int)$stock_minimo, ':stock_actual'=> (int)$stock_actual, ':precio_compra'=> (float)$precio_compra, ':ganancia'=> (float)$ganancia, ':porcentaje_descuento'=> (float)$porcentaje_descuento, ':precio_venta'=> (float)$computedPrecioVenta, ':igv'=> (float)$igv, ':imagen_path'=> $imagen_path ]); if ($ok) { // guardar mensajes en session y PRG $_SESSION['success_msg'] = "Producto guardado OK. Código: $codigo_producto"; if (!empty($warnings)) $_SESSION['warnings'] = $warnings; header("Location: " . $_SERVER['PHP_SELF']); exit; } else { $errors[] = "Error al guardar en la base de datos."; } } } // Si hay warnings de session (post-redirect) if (!empty($warningsFromSession)) $warnings = array_merge($warningsFromSession, $warnings); ?>