LOGIN, REGISTRO, CRUD DE PRODUCTOS Y CATEGORÍAS EN PHP, MYSQL, HTML Y CSS

Un Sistema de Inventario PHP muy básico que opera con MySQL/MariaDB, ideal como base para tu próximo proyecto. Incluye un sistema de autenticación (Login y Registro), una gestión de Productos y Categorías con operaciones CRUD, Búsqueda Dinámica y Paginación.



Acceso y Seguridad del Sistema
El proyecto inicia con el Formulario de Inicio de Sesión, donde el usuario debe ingresar credenciales válidas para acceder al dashboard principal. El sistema también cuenta con una funcionalidad de Registro de Usuarios que garantiza la seguridad al validar que cada correo electrónico sea único y previniendo duplicidades. Una vez autenticado, el usuario tiene acceso completo a la gestión del inventario.

Formulario donde el usuario introduce nombre, correo y contraseña para crear una nueva cuenta en el sistema de inventario.
Formulario de Registro de Nuevo Usuario.

Pantalla o mensaje que confirma la creación exitosa de un nuevo usuario en el sistema de inventario.
Mensaje de éxito tras completar el registro.

Interfaz del formulario de login que solicita el correo electrónico y la contraseña para ingresar al sistema de inventario.
Formulario de Inicio de Sesión.

Dashboard y Gestión de Productos
Al ingresar, el usuario es dirigido al Dashboard, la vista central que lista todos los productos registrados. Esta interfaz está diseñada para la eficiencia, permitiendo al usuario realizar búsquedas dinámicas en tiempo real para filtrar rápidamente grandes volúmenes de datos. Además, la navegación de la lista es fluida gracias a la Paginación, que organiza los resultados en bloques manejables, asegurando una experiencia de usuario rápida y clara.

Captura de pantalla del dashboard de inventario con la tabla principal visible, mostrando columnas como ID, Nombre, Stock, Categoría y Acciones.
Vista principal del Dashboard. Muestra el listado completo de productos, los botones de acción y los controles de búsqueda.

Vista de la tabla de productos mostrando los controles de paginación (números de página y botones Anterior/Siguiente) debajo del listado de productos.
Detalle de la tabla mostrando la Paginación en la parte inferior.

El dashboard con la barra de búsqueda activa, mostrando un solo resultado en la tabla que coincide con el término ingresado por el usuario.
Prueba de la Búsqueda Dinámica. Al ingresar un término, la tabla se actualiza en tiempo real para mostrar solo el producto coincidente.

Operaciones CRUD de Productos
Desde el dashboard, el usuario tiene control total sobre los datos del inventario a través de las operaciones CRUD (Crear, Leer, Actualizar, Eliminar). El usuario puede agregar nuevos productos al sistema, ver la información completa de un producto específico, modificar sus detalles (nombre, stock, categoría, etc.), y eliminar registros cuando sea necesario. Toda esta gestión se realiza con formularios claros que aseguran la correcta introducción y modificación de la información.

Captura de pantalla del formulario de creación de productos, mostrando campos para el nombre, precio, stock y un selector de categoría.
Formulario para la operación CREATE. El usuario ingresa todos los datos del nuevo producto, incluyendo la selección de categoría.

Mensaje de confirmación verde que indica 'Producto agregado exitosamente' en la parte superior del formulario o dashboard.
Confirmación de la operación CREATE. Mensaje de éxito visible después de que el nuevo producto ha sido guardado en la base de datos.

Fila en la tabla del dashboard que muestra el registro del producto recién creado, verificando que los datos fueron guardados correctamente.
Prueba de la operación READ. El producto recién agregado aparece listado en la tabla principal del Dashboard, verificando su ingreso al sistema.

Formulario precargado con los datos del producto existente, mostrando el campo que está siendo editado por el usuario.
Formulario para la operación UPDATE. Un campo del producto está siendo modificado (ej. el Stock o el Nombre), listo para guardar los cambios.

Fila del producto en la tabla del dashboard, mostrando el nuevo valor (ej. el Stock actualizado), confirmando la edición.
Prueba de la operación UPDATE exitosa. La tabla del Dashboard refleja inmediatamente el cambio de datos realizado en el paso de edición.

Diálogo de confirmación con los botones 'Aceptar'/'Eliminar' y 'Cancelar', preguntando al usuario si está seguro de borrar el registro del producto.
Confirmación para la operación DELETE. Un pop-up o mensaje modal pregunta al usuario si realmente desea eliminar el producto, previniendo errores accidentales.

Gestión y Estructura de Categorías
El sistema incluye un módulo dedicado a la Gestión de Categorías, esencial para estructurar y organizar los productos. En esta sección, el usuario puede añadir nuevas categorías y editar las existentes.

Captura de pantalla de la página de gestión de categorías en escritorio, dividida en dos secciones: un formulario para agregar categorías y una tabla listando las categorías actuales con sus botones de edición.
Vista completa de la Gestión de Categorías. Muestra el formulario “Agregar Categoría” a la izquierda y la lista de “Categorías Existentes” a la derecha, en su diseño de dos columnas.



    El proyecto está dividido en varias páginas para que sea más fácil de entender o modificar.
    • db_connect.php
    • logout.php
    • index.php
    • register.php
    • dashboard.php
    • agregar_producto.php
    • editar_producto.php
    • eliminar_producto.php
    • gestion_categorias.php
    • style.css
    SQL

    CREATE DATABASE IF NOT EXISTS `inventariov1`; 
    USE `inventariov1`;
    
    -- Estructura para tabla inventariov1.categorias
    CREATE TABLE IF NOT EXISTS `categorias` (
      `id` int NOT NULL AUTO_INCREMENT,
      `nombre` varchar(100) NOT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `nombre` (`nombre`)
    );
    
    -- Datos para la tabla inventariov1.categorias
    INSERT INTO `categorias` (`id`, `nombre`) VALUES
    	(9, 'Alimentos y Bebidas'),
    	(6, 'Deportes y Fitness'),
    	(4, 'Herramientas y Bricolaje'),
    	(2, 'Hogar y Cocina'),
    	(10, 'Jardinería'),
    	(5, 'Limpieza y Mantenimiento'),
    	(7, 'Moda y Accesorios'),
    	(3, 'Oficina y Papelería'),
    	(8, 'Salud y Belleza'),
    	(1, 'Tecnología');
    
    -- Estructura para tabla inventariov1.productos
    CREATE TABLE IF NOT EXISTS `productos` (
      `id` int NOT NULL AUTO_INCREMENT,
      `nombre` varchar(255) NOT NULL,
      `cantidad` int NOT NULL DEFAULT '0',
      `precio` decimal(10,2) NOT NULL,
      `fecha_creacion` datetime DEFAULT CURRENT_TIMESTAMP,
      `categoria_id` int DEFAULT NULL,
      `fecha_modificacion` datetime DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `categoria_id` (`categoria_id`),
      CONSTRAINT `productos_ibfk_1` FOREIGN KEY (`categoria_id`) REFERENCES `categorias` (`id`) ON DELETE SET NULL
    );
    
    -- Volcando datos para la tabla inventariov1.productos
    INSERT INTO `productos` (`id`, `nombre`, `cantidad`, `precio`, `fecha_creacion`, `categoria_id`, `fecha_modificacion`) VALUES
    	(1, 'Smartwatch Deportivo X4', 25, 120.50, '2025-11-23 02:45:02', 1, NULL),
    	(2, 'Audífonos Inalámbricos Bluetooth', 70, 45.99, '2025-11-23 02:45:02', 1, NULL),
    	(3, 'Tostadora de Acero Inoxidable', 15, 38.00, '2025-11-23 02:45:02', 2, NULL),
    	(4, 'Vajilla de Cerámica (4 Puestos)', 11, 65.90, '2025-11-23 02:45:02', 2, NULL),
    	(5, 'Archivador Metálico 3 Gavetas', 8, 99.00, '2025-11-23 02:45:02', 3, NULL),
    	(6, 'Paquete de Marcadores Permanentes (12)', 95, 11.50, '2025-11-23 02:45:02', 3, NULL),
    	(7, 'Sierra Circular Eléctrica 7"', 9, 135.99, '2025-11-23 02:45:02', 4, NULL),
    	(8, 'Juego de Llaves Fijas Métricas', 40, 32.75, '2025-11-23 02:45:02', 4, NULL),
    	(9, 'Detergente Líquido Concentrado 3L', 150, 12.50, '2025-11-23 02:45:02', 5, NULL),
    	(10, 'Mopa de Microfibra con Spray', 60, 21.90, '2025-11-23 02:45:02', 5, NULL),
    	(11, 'Colchoneta de Yoga Antideslizante', 55, 18.00, '2025-11-23 02:45:02', 6, NULL),
    	(12, 'Mancuernas Ajustables (Par)', 14, 85.00, '2025-11-23 02:45:02', 6, NULL),
    	(13, 'Bufanda de Lana Tejida', 30, 24.99, '2025-11-23 02:45:02', 7, NULL),
    	(14, 'Reloj de Pulsera Clásico Cuero', 17, 78.50, '2025-11-23 02:45:02', 7, NULL),
    	(15, 'Bloqueador Solar FPS 50', 110, 15.75, '2025-11-23 02:45:02', 8, NULL),
    	(16, 'Cepillo Dental Eléctrico', 23, 49.95, '2025-11-23 02:45:02', 8, NULL),
    	(17, 'Café Molido Premium (500g)', 85, 14.00, '2025-11-23 02:45:02', 9, NULL),
    	(18, 'Agua Mineral con Gas (Caja 12)', 200, 9.99, '2025-11-23 02:45:02', 9, NULL),
    	(19, 'Semillas de Tomate Cherry', 180, 2.50, '2025-11-23 02:45:02', 10, NULL),
    	(20, 'Kit de Herramientas de Jardín (3 Pzas)', 35, 19.90, '2025-11-23 02:45:02', 10, NULL);
    
    -- Estructura para tabla inventariov1.usuarios
    CREATE TABLE IF NOT EXISTS `usuarios` (
      `id` int NOT NULL AUTO_INCREMENT,
      `username` varchar(50) DEFAULT NULL,
      `password` varchar(250) DEFAULT NULL,
      PRIMARY KEY (`id`)
    );
    
    -- Estructura datos para la tabla inventariov1.usuarios
    INSERT INTO `usuarios` (`id`, `username`, `password`) VALUES
    	(1, 'test@gmail.com', '$2y$10$Tm5SFPqGQZP4ZpXw7XgicOb3Mo59FN.XvsXOjPbelMMSaUoLD32sC');
    

    db_connect.php

    <?php
    // Reportar errores de MySQLi
    mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
    
    $servername = "localhost";
    $username = "root";
    $password = ""; 
    $dbname = "inventariov1"; // Asegúrate que coincida con tu DB
    
    try 
    {
        // Inicializar la conexión
        $conn = new mysqli($servername, $username, $password, $dbname);
        
        // Establecer el juego de caracteres a UTF-8 para evitar problemas de acentos
        $conn->set_charset("utf8mb4");
    } 
    catch (Exception $e) 
    {
        // Si la conexión falla, se detiene la ejecución
        die("Error de conexión a la base de datos: " . $e->getMessage());
    }
    ?>

    logout.php

    <?php
    // Iniciar la sesión
    session_start();
    
    // Destruir todas las variables de sesión
    $_SESSION = array();
    
    // Destruir la sesión.
    session_destroy();
    
    // Redirigir a la página de inicio de sesión
    header("location: index.php");
    exit;
    ?>
    index.php

    <?php
    // Iniciar la sesión
    session_start();
    if (isset($_SESSION['loggedin']) && $_SESSION['loggedin'] === true) {
        header("location: dashboard.php");
        exit;
    }
    require_once "db_connect.php";
    $username = $password = "";
    $error = "";
    
    // Procesar el formulario
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        $username = trim($_POST["username"]);
        $password = trim($_POST["password"]);
    
        if (empty($username) || empty($password)) {
            $error = "Por favor, ingrese usuario y contraseña.";
        } else {
            $sql = "SELECT id, username, password FROM usuarios WHERE username = ?";
            if ($stmt = $conn->prepare($sql)) {
                $stmt->bind_param("s", $param_username);
                $param_username = $username;
                if ($stmt->execute()) {
                    $stmt->store_result();
                    if ($stmt->num_rows == 1) {                    
                        $stmt->bind_result($id, $username, $hashed_password);
                        if ($stmt->fetch()) {
                            if (password_verify($password, $hashed_password)) {
                                $_SESSION["loggedin"] = true;
                                $_SESSION["id"] = $id;
                                $_SESSION["username"] = $username;                            
                                header("location: dashboard.php");
                                exit;
                            } else {
                                $error = "La contraseña que ingresaste no es válida.";
                            }
                        }
                    } else {
                        $error = "No se encontró ninguna cuenta con ese nombre de usuario.";
                    }
                } else {
                    $error = "Oops! Algo salió mal. Por favor, inténtelo de nuevo más tarde.";
                }
                $stmt->close();
            }
        }
        $conn->close();
    }
    ?>
    
    
    
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    	
        <title>Iniciar Sesión | Sistema de Inventario</title>
        <link rel="stylesheet" href="style.css">
    	
    </head>
    <body>
        <div class="wrapper">
            <h2>Acceso al Sistema</h2>
            <p>Ingrese sus credenciales para continuar.</p>
    
            <?php if (!empty($error)) : ?>
                <p class="error-message"><?php echo htmlspecialchars($error); ?></p>
            <?php endif; ?>
    
            <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
                <div class="input-group">
                    <label for="username">Usuario:</label>
                    <input type="email" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>" required>
                </div>
                
                <div class="input-group">
                    <label for="password-input">Contraseña:</label>
                    <input type="password" id="password-input" name="password" required>
                </div>
    
    				<div class="checkbox-group">
                    <input type="checkbox" id="show-password-check">
                    <label for="show-password-check">Mostrar Contraseña</label>
                </div>
                
                <div class="input-group">
                    <input type="submit" class="btn-primary" value="Ingresar">
                </div>
            </form>
    
            <p class="link-text">¿No tienes una cuenta? <a href="register.php">Regístrate aquí</a>.</p>
        </div>
        
        <script>
            document.getElementById('show-password-check').addEventListener('change', function() {
                var passwordInput = document.getElementById('password-input');
                if (this.checked) {
                    passwordInput.type = 'text';
                } else {
                    passwordInput.type = 'password';
                }
            });
        </script>
    </body>
    </html>
    
    register.php

    <?php
    // 1. Incluir el archivo de conexión
    require_once "db_connect.php";
    
    $username = $password = "";
    $username_err = $password_err = "";
    $success_msg = "";
    
    // 2. Procesar el formulario
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
    
        // 2.1. Validar nombre de usuario
        if (empty(trim($_POST["username"]))) {
            $username_err = "Por favor, ingrese un nombre de usuario.";
        } else {
            // Consultar si el nombre de usuario ya existe
            $sql = "SELECT id FROM usuarios WHERE username = ?";
            
            if ($stmt = $conn->prepare($sql)) {
                $stmt->bind_param("s", $param_username);
                $param_username = trim($_POST["username"]);
                
                if ($stmt->execute()) {
                    $stmt->store_result();
                    
                    if ($stmt->num_rows == 1) {
                        $username_err = "Este nombre de usuario ya está en uso.";
                    } else {
                        $username = trim($_POST["username"]);
                    }
                } else {
                    // Error de consulta a la DB
                    $success_msg = "Error al verificar usuario. Intente de nuevo más tarde.";
                }
                $stmt->close();
            }
        }
    
        // 2.2. Validar contraseña
        if (empty(trim($_POST["password"]))) {
            $password_err = "Por favor, ingrese una contraseña.";
        } elseif (strlen(trim($_POST["password"])) < 6) {
            $password_err = "La contraseña debe tener al menos 6 caracteres.";
        } else {
            $password = trim($_POST["password"]);
        }
        
        // 2.3. Insertar el usuario si no hay errores
        if (empty($username_err) && empty($password_err)) {
            
            // Generar el HASH de la contraseña
            $hashed_password = password_hash($password, PASSWORD_DEFAULT);
            
            $sql = "INSERT INTO usuarios (username, password) VALUES (?, ?)";
            
            if ($stmt = $conn->prepare($sql)) {
                $stmt->bind_param("ss", $param_username, $param_password);
                
                $param_username = $username;
                $param_password = $hashed_password;
                
                if ($stmt->execute()) {
                    $success_msg = "✅ ¡Registro exitoso! Ahora puede <a href='index.php'>iniciar sesión</a>.";
                    // Limpiar los campos para evitar reenvío
                    $username = ""; 
                    $password = "";
                } else {
                    $success_msg = "Error al intentar registrar el usuario: " . $conn->error;
                }
                $stmt->close();
            }
        }
    
        $conn->close();
    }
    ?>
    
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Registro de Usuario | Sistema de Inventario</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div class="wrapper">
            <h2>Registro de Nuevo Usuario</h2>
            
            <?php 
    		
            // Mostrar SOLO el mensaje de éxito si la operación fue exitosa
            if (!empty($success_msg) && strpos($success_msg, '✅') !== false) 
    		{
                echo '<p class="success-message">' . $success_msg . '</p>';
            } else 
    		{ 
    
            ?>
    
            <p>Complete el formulario para crear una cuenta.</p>
    
            <?php if (!empty($success_msg) && strpos($success_msg, 'Error') !== false) : ?>
                <p class="error-message"><?php echo $success_msg; ?></p>
            <?php endif; ?>
    
            <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
                
                <div class="input-group">
                    <label for="username">Usuario:</label>
                    <input type="email" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>">
                    <?php if (!empty($username_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $username_err; ?></p>
                    <?php endif; ?>
                </div>
                
                <div class="input-group">
                    <label for="password-input">Contraseña:</label>
                    <input type="password" id="password-input" name="password">
                    <?php if (!empty($password_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $password_err; ?></p>
                    <?php endif; ?>
                </div>
    
                <div class="checkbox-group">
                    <input type="checkbox" id="show-password-check">
                    <label for="show-password-check">Mostrar Contraseña</label>
                </div>
    
                <div class="input-group">
                    <input type="submit" class="btn-primary" value="Registrar">
                </div>
            </form>
            
            <p class="link-text">¿Ya tienes una cuenta? <a href="index.php">Inicia sesión aquí</a>.</p>
            
            <?php 
          
            } 
            ?>
        </div>
    
        <script>
            document.getElementById('show-password-check').addEventListener('change', function() {
                var passwordInput = document.getElementById('password-input');
                if (this.checked) {
                    passwordInput.type = 'text';
                } else {
                    passwordInput.type = 'password';
                }
            });
        </script>
    </body>
    </html>
    
    dashboard.php

    <?php
    // Iniciar la sesión y verificar la seguridad
    session_start();
    
    if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) 
    {
        header("location: index.php");
        exit;
    }
    
    require_once "db_connect.php";
    
    $username = htmlspecialchars($_SESSION["username"]);
    
    
    // Lógica de Búsqueda 
    $search_term = '';
    $where_clause = '';
    $search_url_param = '';
    $search_param_prepared = null; // Parámetro para la búsqueda con comodines
    
    
    if (isset($_GET['search']) && !empty(trim($_GET['search']))) 
    {
        $search_term = trim($_GET['search']);
        
        // Solo preparamos el valor para el bind_param.
        $search_param_prepared = "%" . $search_term . "%"; 
        
        // La cláusula WHERE solo contiene los placeholders '?'
        $where_clause = " WHERE p.nombre LIKE ? OR c.nombre LIKE ?";
        
        $search_url_param = '&search=' . urlencode($search_term);
    }
    
    // Lógica de Paginación
    $limit = 10; // 10 productos por página
    $page = isset($_GET['page']) && is_numeric($_GET['page']) ? (int)$_GET['page'] : 1;
    $offset = ($page - 1) * $limit;
    
    
    // Obtener el total de registros
    // Filtro de categorías
    $sql_count = "SELECT COUNT(p.id) AS total FROM productos p LEFT JOIN categorias c ON p.categoria_id = c.id" . $where_clause;
    
    
    if ($search_param_prepared) {
        // Usamos sentencia preparada para el COUNT si hay búsqueda
        $stmt_count = $conn->prepare($sql_count);
        // 'ss' = dos parámetros string (para p.nombre y c.nombre)
        $stmt_count->bind_param("ss", $search_param_prepared, $search_param_prepared); 
        $stmt_count->execute();
        $total_result = $stmt_count->get_result();
        $stmt_count->close();
    } else {
        // Si no hay búsqueda, usamos la consulta simple
        $total_result = $conn->query($sql_count);
    }
    
    $total_records = $total_result->fetch_assoc()['total'];
    $total_pages = ceil($total_records / $limit);
    
    
    // Consulta final de Lectura
    // Usamos placeholders '?' también para LIMIT
    $sql_select = "SELECT p.id, p.nombre, p.cantidad, p.precio, p.fecha_creacion, c.nombre AS categoria_nombre 
                   FROM productos p 
                   LEFT JOIN categorias c ON p.categoria_id = c.id";
                   
    // Consulta final con WHERE y LIMIT con placeholders
    $sql_final = $sql_select . $where_clause . " ORDER BY p.id DESC LIMIT ?, ?";
    
    $stmt = $conn->prepare($sql_final);
    
    // Determinar los tipos de parámetros para bind_param
    if ($search_param_prepared) {
        // Si hay búsqueda: ss (para los 2 WHERE) + ii (para LIMIT)
        $stmt->bind_param("ssii", $search_param_prepared, $search_param_prepared, $offset, $limit);
    } else {
        // Si NO hay búsqueda: ii (solo para LIMIT)
        $stmt->bind_param("ii", $offset, $limit);
    }
    
    $stmt->execute();
    $result = $stmt->get_result();
    $stmt->close();
    
    $conn->close();
    ?>
    
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Dashboard | Inventario</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body class="dashboard-body">
        
        <!-- Header -->
        <header class="header">
            <h1>Sistema de Inventario PHP</h1>
            <div class="nav-links">
                <span>Bienvenido, <strong><?php echo $username; ?></strong></span>
                <a href="logout.php">Cerrar Sesión</a>
            </div>
        </header>
    
        <!-- Contenido Principal -->
        <div class="main-content">
            
            <!-- Bar: Título, Búsqueda y Botones -->
    <div class="action-bar">
        
        <div class="action-bar-row">
            <h2>Inventario de Productos</h2>
            
            <div class="action-buttons-group">
                <a href="agregar_producto.php" class="btn-primary">Agregar Producto</a>
                <a href="gestion_categorias.php" class="btn-primary" style="background-color: #007bff;">
                    Gestionar Categorías
                </a>
            </div>
        </div>
        
        <form method="GET" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" class="search-form">
            <input type="text" name="search" placeholder="Buscar producto o categoría..." value="<?php echo htmlspecialchars($search_term); ?>">
            <button type="submit" class="btn-search">🔍</button>
            <?php if (!empty($search_term)): ?>
                <a href="dashboard.php" class="btn-clear-search">X</a>
            <?php endif; ?>
        </form>
    </div>
    
            <!-- Tabla de Inventario -->
            <div class="table-responsive">
                <?php if ($result->num_rows > 0): ?>
                    <table class="inventory-table">
                        <thead>
                            <tr>
                                <th>ID</th>
                                <th>Producto</th>
                                <th>Cantidad</th>
                                <th>Precio</th>
                                <th>Categoría</th>
                                <th>Creado</th>
                                <th>Acciones</th>
                            </tr>
                        </thead>
                        <tbody>
                            <?php while ($row = $result->fetch_assoc()): ?>
                                <tr>
                                    <td data-label="ID"><?php echo htmlspecialchars($row['id']); ?></td>
                                    <td data-label="Producto"><?php echo htmlspecialchars($row['nombre']); ?></td>
                                    <td data-label="Cantidad"><?php echo htmlspecialchars($row['cantidad']); ?></td>
                                    <td data-label="Precio">$<?php echo number_format($row['precio'], 2); ?></td>
                                    <td data-label="Categoría">
                                        <?php echo htmlspecialchars($row['categoria_nombre'] ?? 'Sin Categoría'); ?>
                                    </td>
                                    <td data-label="Creado"><?php echo date('d/m/Y', strtotime($row['fecha_creacion'])); ?></td>
                                    <td data-label="Acciones">
                                        <a href='editar_producto.php?id=<?php echo $row['id']; ?>' class='btn-small btn-edit'>Editar</a>
                                        <a href='eliminar_producto.php?id=<?php echo $row['id']; ?>' class='btn-small btn-delete' onclick="return confirm('¿Está seguro de eliminar este producto?');">Eliminar</a>
                                    </td>
                                </tr>
                            <?php endwhile; ?>
                        </tbody>
                    </table>
    
                    <!-- Paginación -->
                    <div class="pagination">
                        <?php if ($total_pages > 1): ?>
                            <?php for ($i = 1; $i <= $total_pages; $i++): ?>
                                <a 
                                    href="?page=<?php echo $i . $search_url_param; ?>" 
                                    class="<?php echo ($i == $page) ? 'active' : ''; ?>"
                                >
                                    <?php echo $i; ?>
                                </a>
                            <?php endfor; ?>
                        <?php endif; ?>
                    </div>
    
                <?php else: ?>
                    <p class="no-records">
                        <?php echo !empty($search_term) ? "No se encontraron productos o categorías que coincidan con: <strong>" . htmlspecialchars($search_term) . "</strong>." : "Aún no hay productos en el inventario."; ?>
                    </p>
                <?php endif; ?>
            </div>
    
        </div>
    </body>
    </html>
    

    agregar_producto.php

    <?php
    // Lógica de Seguridad y Conexión
    session_start();
    
    if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
        header("location: index.php");
        exit;
    }
    
    require_once "db_connect.php";
    
    $nombre = $cantidad = $precio = $categoria_id = "";
    $nombre_err = $cantidad_err = $precio_err = $categoria_id_err = "";
    $success_msg = "";
    
    // Obtener la lista de categorías (para el dropdown)
    $categorias = [];
    $categorias_result = $conn->query("SELECT id, nombre FROM categorias ORDER BY nombre");
    if ($categorias_result) {
        while ($row = $categorias_result->fetch_assoc()) {
            $categorias[] = $row;
        }
    }
    // Si la consulta falló, $categorias queda vacío, lo cual se maneja en el HTML.
    
    
    // Procesar el formulario
    if ($_SERVER["REQUEST_METHOD"] == "POST") {
        
        // Validar y sanitizar los campos
        
        // Validar Nombre
        if (empty(trim($_POST["nombre"]))) {
            $nombre_err = "Ingrese el nombre del producto.";
        } else {
            $nombre = trim($_POST["nombre"]);
        }
    
        // Validar Cantidad
        $input_cantidad = trim($_POST["cantidad"]);
        if (empty($input_cantidad)) {
            $cantidad_err = "Ingrese la cantidad.";
        } elseif (!filter_var($input_cantidad, FILTER_VALIDATE_INT) || $input_cantidad < 0) {
            $cantidad_err = "La cantidad debe ser un número entero positivo o cero.";
        } else {
            $cantidad = (int)$input_cantidad;
        }
    
        // Validar Precio
        $input_precio = trim($_POST["precio"]);
        if (empty($input_precio)) {
            $precio_err = "Ingrese el precio unitario.";
        } elseif (!is_numeric($input_precio) || $input_precio <= 0) {
            $precio_err = "El precio debe ser un número positivo.";
        } else {
            $precio = str_replace(',', '.', $input_precio); // Formatear el precio
            $precio = (double)$precio;
        }
    
        // Validar Categoría
        $input_categoria_id = $_POST["categoria_id"];
        if (empty($input_categoria_id) || !is_numeric($input_categoria_id)) {
            $categoria_id_err = "Seleccione una categoría válida.";
        } else {
            $categoria_id = (int)$input_categoria_id;
        }
    
        // Ejecutar la Inserción si no hay errores
        if (empty($nombre_err) && empty($cantidad_err) && empty($precio_err) && empty($categoria_id_err)) {
            
            // Sentencia INSERT con placeholders para 5 valores
            $sql = "INSERT INTO productos (nombre, cantidad, precio, categoria_id, fecha_creacion) VALUES (?, ?, ?, ?, NOW())";
            
            if ($stmt = $conn->prepare($sql)) {
                // "sidi" = string, integer, double, integer
                $stmt->bind_param("sidi", $param_nombre, $param_cantidad, $param_precio, $param_categoria_id);
                
                // Asignar parámetros
                $param_nombre = $nombre;
                $param_cantidad = $cantidad;
                $param_precio = $precio;
                $param_categoria_id = $categoria_id; 
                
                if ($stmt->execute()) {
                    $success_msg = "✅ Producto **" . htmlspecialchars($nombre) . "** agregado correctamente.";
                    
                    // Limpiar variables después de la inserción exitosa
                    $nombre = $cantidad = $precio = $categoria_id = "";
    
                } else {
                    $success_msg = "Error al intentar agregar el producto: " . $conn->error;
                }
                $stmt->close();
            }
        }
    }
    $conn->close();
    
    $username = htmlspecialchars($_SESSION["username"]);
    ?>
    
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Agregar Producto | Inventario</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body class="dashboard-body">
        
        <header class="header">
            <h1>Sistema de Inventario PHP</h1>
            <div class="nav-links">
                <span>Bienvenido, <strong><?php echo $username; ?></strong></span>
                <a href="logout.php">Cerrar Sesión</a>
            </div>
        </header>
    
        <div class="main-content" style="max-width: 600px;"> 
            
            <div style="margin-bottom: 15px; text-align: left;">
                <a href="dashboard.php" class="btn-primary" style="width: auto; background-color: #6c757d;">&larr; Volver al Inventario</a>
            </div>
    
            <div class="action-bar" style="border-bottom: 2px solid #ced4da; padding-bottom: 10px; justify-content: flex-start;">
                <h2>Agregar Nuevo Producto</h2>
            </div>
    
            <?php 
            // Mostrar mensaje de éxito o error
            if (!empty($success_msg)) : ?>
                <p class="<?php echo (strpos($success_msg, '✅') !== false) ? 'success-message' : 'error-message'; ?>" style="text-align: center;"><?php echo $success_msg; ?></p>
            <?php endif; ?>
    
            <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
                
                <div class="input-group">
                    <label for="nombre">Nombre del Producto:</label>
                    <input type="text" id="nombre" name="nombre" value="<?php echo htmlspecialchars($nombre); ?>">
                    <?php if (!empty($nombre_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $nombre_err; ?></p>
                    <?php endif; ?>
                </div>
                
                <div class="input-group">
                    <label for="cantidad">Cantidad en Stock:</label>
                    <input type="number" id="cantidad" name="cantidad" value="<?php echo htmlspecialchars($cantidad); ?>">
                    <?php if (!empty($cantidad_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $cantidad_err; ?></p>
                    <?php endif; ?>
                </div>
    
                <div class="input-group">
                    <label for="precio">Precio Unitario:</label>
                    <input type="text" id="precio" name="precio" placeholder="Ej: 19.99" value="<?php echo htmlspecialchars($precio); ?>">
                    <?php if (!empty($precio_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $precio_err; ?></p>
                    <?php endif; ?>
                </div>
                
                <div class="input-group">
                    <label for="categoria">Categoría:</label>
                    <select id="categoria" name="categoria_id" class="<?php echo (!empty($categoria_id_err)) ? 'input-error' : ''; ?>">
                        <option value="">-- Seleccione Categoría --</option>
                        <?php 
                        if (!empty($categorias)) {
                            foreach ($categorias as $cat): 
                                $selected = ($cat['id'] == $categoria_id) ? 'selected' : '';
                            ?>
                                <option value="<?= $cat['id'] ?>" <?= $selected ?>><?= htmlspecialchars($cat['nombre']) ?></option>
                            <?php 
                            endforeach; 
                        } else {
                            // Mensaje si no hay categorías creadas
                            echo "<option value='' disabled>No hay categorías. Cree una primero.</option>";
                        }
                        ?>
                    </select>
                    <?php if (!empty($categoria_id_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $categoria_id_err; ?></p>
                    <?php endif; ?>
                </div>
    
                <div class="input-group">
                    <input type="submit" class="btn-primary" value="Agregar Producto">
                </div>
            </form>
    
        </div>
    </body>
    </html>
    
    editar_producto.php

    <?php
    // Lógica de Seguridad y Conexión
    session_start();
    
    if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
        header("location: index.php");
        exit;
    }
    
    require_once "db_connect.php";
    
    $nombre = $cantidad = $precio = $categoria_id = "";
    $nombre_err = $cantidad_err = $precio_err = $categoria_id_err = "";
    
    // Obtener la lista de categorías (para el dropdown)
    $categorias = [];
    $categorias_result = $conn->query("SELECT id, nombre FROM categorias ORDER BY nombre");
    if ($categorias_result) {
        while ($row = $categorias_result->fetch_assoc()) {
            $categorias[] = $row;
        }
    }
    
    
    // Procesar datos al enviar el formulario (UPDATE)
    if (isset($_POST["id"]) && !empty($_POST["id"])) {
        $id = $_POST["id"]; // El ID del producto que se está editando
        
        // Validar y sanitizar los campos (Similar a agregar_producto.php xd)
        
        // Validar Nombre
        if (empty(trim($_POST["nombre"]))) {
            $nombre_err = "Ingrese el nombre del producto.";
        } else {
            $nombre = trim($_POST["nombre"]);
        }
    
        // Validar Cantidad
        $input_cantidad = trim($_POST["cantidad"]);
        if (empty($input_cantidad)) {
            $cantidad_err = "Ingrese la cantidad.";
        } elseif (!filter_var($input_cantidad, FILTER_VALIDATE_INT) || $input_cantidad < 0) {
            $cantidad_err = "La cantidad debe ser un número entero positivo o cero.";
        } else {
            $cantidad = (int)$input_cantidad;
        }
    
        // Validar Precio
        $input_precio = trim($_POST["precio"]);
        if (empty($input_precio)) {
            $precio_err = "Ingrese el precio unitario.";
        } elseif (!is_numeric($input_precio) || $input_precio <= 0) {
            $precio_err = "El precio debe ser un número positivo.";
        } else {
            $precio = str_replace(',', '.', $input_precio); // Formatear el precio
            $precio = (double)$precio;
        }
    
        // Validar Categoría
        $input_categoria_id = $_POST["categoria_id"];
        if (empty($input_categoria_id) || !is_numeric($input_categoria_id)) {
            // Permitimos que sea NULL, solo verificamos que si se envía un valor, sea un ID válido.
            // Si el usuario deja el "-- Seleccione Categoría --" y no tiene categoría, podría ser 0 o un string vacío, 
            // por lo que ajustamos la lógica de validación para permitir NULL si es necesario.
            $categoria_id = null; 
        } else {
            $categoria_id = (int)$input_categoria_id;
        }
    
    
        // Ejecutar la Actualización si no hay errores
        if (empty($nombre_err) && empty($cantidad_err) && empty($precio_err)) { // categoria_id ahora permite ser NULL
            
            // Sentencia UPDATE con placeholders para 4 valores (nombre, cantidad, precio, categoria_id) + 1 para el ID
            $sql = "UPDATE productos SET nombre=?, cantidad=?, precio=?, categoria_id=?, fecha_modificacion=NOW() WHERE id=?";
            
            if ($stmt = $conn->prepare($sql)) {
                // "sidi" = string, integer, double, integer, (y el último 'i' es el ID del WHERE)
                $stmt->bind_param("sidii", $param_nombre, $param_cantidad, $param_precio, $param_categoria_id, $param_id);
                
                // Asignar parámetros
                $param_nombre = $nombre;
                $param_cantidad = $cantidad;
                $param_precio = $precio;
                // Usar NULL para la categoría si no se seleccionó ninguna
                $param_categoria_id = $categoria_id; 
                $param_id = $id;
                
                if ($stmt->execute()) {
                    header("location: dashboard.php"); // Redirigir después de la edición exitosa
                    exit();
                } else {
                    echo "<p class='error-message'>Error al intentar actualizar el producto: " . $conn->error . "</p>";
                }
                $stmt->close();
            }
        }
    
    } else {
        // Lógica para CARGAR los datos iniciales al entrar a la página
        if (isset($_GET["id"]) && !empty(trim($_GET["id"]))) {
            $id = trim($_GET["id"]);
            
            // Preparamos la consulta para obtener los datos del producto
            $sql = "SELECT id, nombre, cantidad, precio, categoria_id FROM productos WHERE id = ?";
            
            if ($stmt = $conn->prepare($sql)) {
                $stmt->bind_param("i", $param_id);
                $param_id = $id;
                
                if ($stmt->execute()) {
                    $result = $stmt->get_result();
                    
                    if ($result->num_rows == 1) {
                        // Obtener la fila de resultados
                        $row = $result->fetch_assoc();
                        
                        // Asignar los valores a las variables del formulario
                        $nombre = $row["nombre"];
                        $cantidad = $row["cantidad"];
                        $precio = $row["precio"];
                        // Cargar el ID de la categoría actual del producto
                        $categoria_id = $row["categoria_id"]; 
    
                    } else {
                        // El producto no existe
                        echo "<p class='error-message'>Producto no encontrado.</p>";
                        exit();
                    }
                } else {
                    echo "<p class='error-message'>Oops! Algo salió mal. Intente de nuevo más tarde.</p>";
                    exit();
                }
                $stmt->close();
            }
        } else {
            // No hay ID en la URL
            header("location: dashboard.php");
            exit();
        }
    }
    
    $conn->close();
    $username = htmlspecialchars($_SESSION["username"]);
    ?>
    
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Editar Producto | Inventario</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body class="dashboard-body">
        
        <header class="header">
            <h1>Sistema de Inventario PHP</h1>
            <div class="nav-links">
                <span>Bienvenido, <strong><?php echo $username; ?></strong></span>
                <a href="logout.php">Cerrar Sesión</a>
            </div>
        </header>
    
        <div class="main-content" style="max-width: 600px;"> 
            
            <div style="margin-bottom: 15px; text-align: left;">
                <a href="dashboard.php" class="btn-primary" style="width: auto; background-color: #6c757d;">&larr; Volver al Inventario</a>
            </div>
    
            <div class="action-bar" style="border-bottom: 2px solid #ced4da; padding-bottom: 10px; justify-content: flex-start;">
                <h2>Editar Producto #<?php echo htmlspecialchars($id); ?></h2>
            </div>
    
            <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="post">
                
                <input type="hidden" name="id" value="<?php echo $id; ?>">
    
                <div class="input-group">
                    <label for="nombre">Nombre del Producto:</label>
                    <input type="text" id="nombre" name="nombre" value="<?php echo htmlspecialchars($nombre); ?>">
                    <?php if (!empty($nombre_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $nombre_err; ?></p>
                    <?php endif; ?>
                </div>
                
                <div class="input-group">
                    <label for="cantidad">Cantidad en Stock:</label>
                    <input type="number" id="cantidad" name="cantidad" value="<?php echo htmlspecialchars($cantidad); ?>">
                    <?php if (!empty($cantidad_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $cantidad_err; ?></p>
                    <?php endif; ?>
                </div>
    
                <div class="input-group">
                    <label for="precio">Precio Unitario:</label>
                    <input type="text" id="precio" name="precio" placeholder="Ej: 19.99" value="<?php echo htmlspecialchars($precio); ?>">
                    <?php if (!empty($precio_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $precio_err; ?></p>
                    <?php endif; ?>
                </div>
                
                <div class="input-group">
                    <label for="categoria">Categoría:</label>
                    <select id="categoria" name="categoria_id" class="<?php echo (!empty($categoria_id_err)) ? 'input-error' : ''; ?>">
                        <option value="">-- Seleccione Categoría --</option>
                        <?php 
                        if (!empty($categorias)) {
                            foreach ($categorias as $cat): 
                                // Clave para la edición: Compara el ID de la categoría actual con el ID de la categoría en el bucle
                                $selected = ($cat['id'] == $categoria_id) ? 'selected' : '';
                            ?>
                                <option value="<?= $cat['id'] ?>" <?= $selected ?>><?= htmlspecialchars($cat['nombre']) ?></option>
                            <?php 
                            endforeach; 
                        } else {
                            echo "<option value='' disabled>No hay categorías. Cree una primero.</option>";
                        }
                        ?>
                    </select>
                    <?php if (!empty($categoria_id_err)) : ?>
                        <p class="error-message" style="margin-top: 5px;"><?php echo $categoria_id_err; ?></p>
                    <?php endif; ?>
                </div>
    
                <div class="input-group">
                    <input type="submit" class="btn-primary" value="Guardar Cambios">
                </div>
            </form>
    
        </div>
    </body>
    </html>
    
    eliminar_producto.php

    <?php
    // Seguridad y Conexión
    session_start();
    
    // Redirigir si el usuario no está logueado
    if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
        header("location: index.php");
        exit;
    }
    
    require_once "db_connect.php";
    
    // Verificar que se haya pasado un ID válido por GET
    if (isset($_GET["id"]) && !empty(trim($_GET["id"]))) {
        
        // Obtener y sanear el ID del producto
        $id = trim($_GET["id"]);
        
        // Sentencia DELETE preparada (Máxima seguridad)
        $sql = "DELETE FROM productos WHERE id = ?";
        
        if ($stmt = $conn->prepare($sql)) {
            $stmt->bind_param("i", $param_id);
            $param_id = $id;
    
            if ($stmt->execute()) {
                // Éxito: Producto eliminado. Redirigir al dashboard.
                
                // Usamos una variable de sesión para mostrar un mensaje de éxito en dashboard.php
                $_SESSION['delete_success'] = "✅ El producto con ID #$id ha sido eliminado exitosamente.";
    
                header("location: dashboard.php");
                exit;
            } else {
                // Error al ejecutar la consulta
                echo "Error al intentar eliminar el producto: " . $conn->error;
            }
            $stmt->close();
        }
    } else {
        // Si no se pasó el ID, redirigir al dashboard.
        header("location: dashboard.php");
        exit;
    }
    
    $conn->close();
    ?>
    
    gestion_categorias.php

    <?php
    // Lógica de Seguridad y Conexión
    session_start();
    
    if (!isset($_SESSION["loggedin"]) || $_SESSION["loggedin"] !== true) {
        header("location: index.php");
        exit;
    }
    
    require_once 'db_connect.php'; 
    
    // VARIABLES DE ESTADO Y DATOS DE EDICIÓN
    $message = "";
    $nombre = ""; 
    $categoria_id_a_editar = null; 
    $modo_edicion = false;
    $username = htmlspecialchars($_SESSION["username"]);
    
    
    // DETECCIÓN Y CARGA DEL MODO EDICIÓN (GET)
    if (isset($_GET['action']) && $_GET['action'] == 'edit' && isset($_GET['id'])) {
        $categoria_id_a_editar = (int)$_GET['id'];
        
        // Consulta para cargar los datos de la categoría
        $stmt = $conn->prepare("SELECT nombre FROM categorias WHERE id = ?");
        $stmt->bind_param("i", $categoria_id_a_editar);
        $stmt->execute();
        $result = $stmt->get_result();
        
        if ($result->num_rows == 1) {
            $row = $result->fetch_assoc();
            $nombre = $row['nombre']; // Cargar el nombre actual en el formulario
            $modo_edicion = true; // Activar el modo edición
        } else {
            $message = "<div class='error-message'>Categoría no encontrada.</div>";
        }
        $stmt->close();
    }
    
    // PROCESAMIENTO DEL FORMULARIO (POST) - AGREGAR o EDITAR
    if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['nombre'])) {
        $nombre = trim($_POST['nombre']);
        // Usamos el ID oculto del formulario, si existe, para saber si es edición
        $categoria_id_post = $_POST['categoria_id'] ?? null; 
        
        if (empty($nombre)) {
            $message = "<div class='error-message'>El nombre de la categoría no puede estar vacío.</div>";
        } else {
            if ($categoria_id_post) {
                // LÓGICA DE EDICIÓN (UPDATE)
                $stmt = $conn->prepare("UPDATE categorias SET nombre = ? WHERE id = ?");
                $stmt->bind_param("si", $nombre, $categoria_id_post);
                
                if ($stmt->execute()) {
                    // Si la ejecución fue exitosa Y se afectó alguna fila (opcionalmente)
                    if ($stmt->affected_rows > 0) {
                        // Redirigir para salir del modo edición y mostrar mensaje de éxito limpio
                        header("Location: gestion_categorias.php?msg=edit_ok");
                        exit;
                    } else {
                        // Si el nombre no cambió, también se considera éxito o se maneja como error
                        header("Location: gestion_categorias.php?msg=no_change");
                        exit;
                    }
                } else {
                    // === INICIO DE CORRECCIÓN PARA UPDATE (Duplicidad) ===
                    if ($stmt->errno == 1062) { 
                        $message = "<div class='error-message'>Error: Ya existe otra categoría registrada con el nombre '<strong>" . htmlspecialchars($nombre) . "</strong>'.</div>";
                    } else {
                        $message = "<div class='error-message'>Error al actualizar categoría: " . $stmt->error . "</div>";
                    }
                    // === FIN DE CORRECCIÓN PARA UPDATE ===
                }
          } else {
                // LÓGICA DE AGREGAR (INSERT)
                $stmt = $conn->prepare("INSERT INTO categorias (nombre) VALUES (?)");
                $stmt->bind_param("s", $nombre);
                
                // === INICIO DEL MANEJO DE EXCEPCIONES ===
                try {
                    if ($stmt->execute()) {
                        $message = "<div class='success-message'>✅ Categoría '<strong>" . htmlspecialchars($nombre) . "</strong>' agregada exitosamente.</div>";
                        $nombre = ""; // Limpiar el campo
                    } else {
                        // Esta parte maneja otros errores de ejecución que no son excepciones (aunque es menos común)
                        $message = "<div class='error-message'>Error al agregar categoría: " . $stmt->error . "</div>";
                    }
                } catch (mysqli_sql_exception $e) {
                    // Capturamos el error de SQL y verificamos si es duplicidad (código 1062)
                    if ($e->getCode() === 1062) {
                        $message = "<div class='error-message'>Error: La categoría '<strong>" . htmlspecialchars($nombre) . "</strong>' ya existe. Por favor, elige un nombre diferente.</div>";
                    } else {
                        // Manejamos cualquier otra excepción SQL
                        $message = "<div class='error-message'>Error de base de datos: (" . $e->getCode() . ") " . $e->getMessage() . "</div>";
                    }
                }
                // === FIN DEL MANEJO DE EXCEPCIONES ===
                
            } // Fin del else (LÓGICA DE AGREGAR)
            $stmt->close();
        }
    }
    
    // Lógica para mostrar mensaje de éxito de edición (después de redirección)
    if (isset($_GET['msg'])) {
        if ($_GET['msg'] == 'edit_ok') {
            $message = "<div class='success-message'>✅ Categoría actualizada exitosamente.</div>";
        } elseif ($_GET['msg'] == 'no_change') {
            $message = "<div class='alert-info'>ℹ️ No se realizaron cambios en la categoría.</div>";
        }
    }
    
    
    // OBTENER LA LISTA DE CATEGORÍAS (PARA LA TABLA DE LISTADO)
    $categorias_list = [];
    $result_list = $conn->query("SELECT id, nombre FROM categorias ORDER BY nombre ASC");
    
    if ($result_list) {
        while ($row = $result_list->fetch_assoc()) {
            $categorias_list[] = $row;
        }
        $result_list->free();
    }
    
    // Nota: Hemos quitado $conn->close() de aquí porque ya lo estás cerrando al final del bloque PHP.
    // Es mejor mantenerlo al final si no hay más código PHP que lo necesite.
    $conn->close();
    ?>
    
    <!DOCTYPE html>
    <html lang="es">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title><?php echo $modo_edicion ? 'Editar Categoría' : 'Gestión de Categorías'; ?></title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body class="dashboard-body">
        
        <header class="header">
            <h1>Sistema de Inventario PHP</h1>
            <div class="nav-links">
                <span>Bienvenido, <strong><?php echo $username; ?></strong></span>
                <a href="logout.php">Cerrar Sesión</a>
            </div>
        </header>
    
    <div class="main-content gestion-categorias-layout">
            
           <div class="columna-izquierda">
                <div style="margin-bottom: 15px; text-align: left;">
                    <a href="dashboard.php" class="btn-primary" style="width: auto; background-color: #6c757d;">&larr; Volver al Dashboard</a>
                </div>
                
                <div class="action-bar" style="border-bottom: 2px solid #ced4da; padding-bottom: 10px; justify-content: flex-start;">
                    <h2><?php echo $modo_edicion ? 'Editar Categoría' : 'Agregar Categoría'; ?></h2>
                    <?php if ($modo_edicion): ?>
                        <a href="gestion_categorias.php" class="btn-primary" style="margin-left: auto; background-color: #f7a300;">Cancelar Edición</a>
                    <?php endif; ?>
                </div>
    
                <?php echo $message; // Mostrar mensaje de éxito o error ?>
                
                <form action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>" method="POST">
                    
                    <?php if ($modo_edicion): ?>
                        <input type="hidden" name="categoria_id" value="<?php echo $categoria_id_a_editar; ?>">
                    <?php endif; ?>
    
                    <div class="input-group">
                        <label for="nombre">Nombre de la Categoría:</label>
                        <input type="text" id="nombre" name="nombre" value="<?php echo htmlspecialchars($nombre); ?>" required>
                    </div>
                    
                    <div class="input-group">
                        <button type="submit" class="btn-primary">
                            <?php echo $modo_edicion ? 'Guardar Cambios' : 'Guardar Categoría'; ?>
                        </button>
                    </div>
                </form>
            </div>
            
           <div class="columna-derecha">
                <div class="action-bar" style="border-bottom: 2px solid #ced4da; padding-bottom: 10px; justify-content: flex-start;">
                    <h2>Categorías Existentes (<?php echo count($categorias_list); ?>)</h2>
                </div>
    
                <?php if (!empty($categorias_list)): ?>
                    <div class="table-responsive">
                      <table class="inventory-table category-table-responsive" style="margin-top: 0;">
                            <thead>
                                <tr>
                                    <th style="width: 15%;">ID</th>
                                    <th style="width: 55%;">Nombre</th>
                                    <th style="width: 30%;">Acciones</th>
                                </tr>
                            </thead>
                            <tbody>
                                <?php foreach ($categorias_list as $cat): ?>
                                    <tr>
                                        <td data-label="ID" style="text-align: center;"><?php echo htmlspecialchars($cat['id']); ?></td>
                                        <td data-label="Nombre"><?php echo htmlspecialchars($cat['nombre']); ?></td>
                                        <td data-label="Acciones">
                                            <a href='gestion_categorias.php?action=edit&id=<?php echo $cat['id']; ?>' class='btn-small btn-edit'>Editar</a>
                                        </td>
                                    </tr>
                                <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                <?php else: ?>
                    <p style="text-align: center; color: #6c757d;">Aún no hay categorías creadas.</p>
                <?php endif; ?>
            </div>
            
        </div>
    </body>
    </html>
    

    style.css

    /* Estilos Base y Reutilizables */
    body {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        background-color: #e9ecef;
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 100vh;
        margin: 0;
    }
    
    /* Contenedor principal para formularios (Login y Registro) */
    .wrapper {
        background-color: #ffffff;
        padding: 40px;
        border-radius: 12px; 
        box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); 
        width: 100%;
        max-width: 380px;
        text-align: center;
        transition: all 0.3s ease-in-out;
    }
    
    h2, h1 {
        color: #212529; 
        margin-bottom: 25px;
        border-bottom: 3px solid #007bff; /* Línea de acento azul */
        padding-bottom: 15px;
        font-weight: 600; 
    }
    
    /*  Estilos de Formulario (Común a todos) */
    .input-group {
        margin-bottom: 20px;
        text-align: left;
        position: relative;
    }
    
    label {
        display: block;
        margin-bottom: 8px;
        font-weight: 500;
        color: #495057;
    }
    .btn-search {
    	flex-grow: 0;  
        flex-shrink: 0;
        /* Asegura que la altura del botón coincida con el input */
        height: 40px; 
        padding: 0 12px; 
        background-color: #007bff;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 1em;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    
    /* Estilo del Botón de Borrado (X) */
    .btn-clear-search {
        flex-grow: 0; 
        flex-shrink: 0; 
        display: flex; 
        align-items: center;
        justify-content: center;
        height: 40px; 
        padding: 0 16px;
        background-color: #dc3545; 
        color: white;
        text-decoration: none;
        border-radius: 4px;
        font-weight: bold;
        line-height: 1; 
    }
    .search-form {
        display: flex;
        align-items: center;
        gap: 5px; 
        width: 100%; 
        max-width: none;
        margin: 0;
        flex-wrap: nowrap; 
    }
    .search-form input[type="text"] {
        
    	flex-grow: 1; 
        flex-shrink: 1; 
        width: auto !important; 
        box-sizing: border-box; 
        height: 40px; 
        padding: 10px;
    }
    input[type="text"],
    input[type="email"],
    input[type="password"],
    input[type="number"],
    textarea {
    
        width: 100%;
        padding: 12px;
        border: 1px solid #ced4da;
        border-radius: 6px;
        box-sizing: border-box; 
        transition: border-color 0.3s, box-shadow 0.3s;
        /* Eliminar flechas de número en navegadores */
        -moz-appearance: textfield; 
    }
    
    /* Para navegadores (Chrome, Safari) */
    input[type="number"]::-webkit-inner-spin-button, 
    input[type="number"]::-webkit-outer-spin-button { 
        -webkit-appearance: none;
        margin: 0;
    }
    
    input[type="text"]:focus,
    input[type="email"]:focus,
    input[type="password"]:focus,
    input[type="number"]:focus,
    textarea:focus {
        border-color: #007bff; 
        box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
        outline: none;
    }
    
    /* Estilos para el Checkbox (Mostrar Contraseña) */
    .checkbox-group {
        text-align: left;
        margin-top: -10px;
        margin-bottom: 20px;
        font-size: 0.95em;
        color: #495057;
    }
    
    /* Asegura que el texto y el check estén al lado (Inline) */
    .checkbox-group label {
        display: inline-block;
        margin-left: 5px;
        margin-bottom: 0;
        font-weight: normal; 
        cursor: pointer;
    }
    
    .checkbox-group input[type="checkbox"] {
        width: auto;
        margin: 0;
        vertical-align: middle;
        padding: 0;
        height: auto;
    }
    
    /*  Estilos de Botones */
    .btn-primary {
        background-color: #007bff; 
        color: white;
    	padding: 10px 15px; 
        font-size: 1em;
        font-weight: bold;
        text-decoration: none;
        text-align: center;
        border-radius: 4px;
        width: auto;
        text-align: center;
        font-size: 1em;
        font-weight: 600;
        transition: background-color 0.3s, transform 0.1s;
        margin-top: 20px; 
        text-decoration: none; 
        display: inline-block;
     
    }
    
    .btn-primary:hover {
        background-color: #0056b3;
        transform: translateY(-1px);
    }
    
    /*  Mensajes y Enlaces */
    .error-message {
        color: #721c24; 
        background-color: #f8d7da;
        border: 1px solid #f5c6cb;
        padding: 10px 12px;
        border-radius: 6px;
        margin-bottom: 15px;
        text-align: left;
        font-size: 0.9em;
    }
    
    .success-message {
        color: #155724; 
        background-color: #d4edda;
        border: 1px solid #c3e6cb;
        padding: 12px;
        border-radius: 6px;
        margin-bottom: 15px;
        text-align: left;
        font-weight: 500;
    }
    
    .link-text {
        margin-top: 20px;
        font-size: 0.95em;
        color: #6c757d;
    }
    
    .link-text a {
        color: #007bff;
        text-decoration: none;
        font-weight: 600;
    }
    
    .link-text a:hover {
        text-decoration: underline;
    }
    
    /*  Estilos del Dashboard (CRUD) */
    
    body.dashboard-body {
        display: block; 
        background-color: #e9ecef;
    }
    
    /* Estilos del Navegador/Header */
    .header {
        width: 100%;
        background-color: #007bff;
        color: white;
        padding: 15px 50px;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        position: fixed;
        top: 0;
        left: 0;
        z-index: 100;
        display: flex;
        justify-content: space-between;
        align-items: center;
        box-sizing: border-box;
    }
    
    .header h1 {
        color: white;
        margin: 0;
        font-size: 1.5em;
        border-bottom: none;
        padding-bottom: 0;
    }
    
    .nav-links a {
        color: white;
        text-decoration: none;
        margin-left: 20px;
        font-weight: 500;
        transition: opacity 0.3s;
    }
    
    .nav-links a:hover {
        opacity: 0.8;
    }
    
    /* Contenido Principal */
    .main-content {
        margin-top: 80px; 
        padding: 20px;
        width: 100%;
        max-width: 1200px; 
        margin-left: auto;
        margin-right: auto;
        background-color: #ffffff;
        border-radius: 8px;
        box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
        min-height: calc(100vh - 100px);
    }
    
    /* Estilo para los botones de acción dentro del dashboard */
    .action-bar {
    display: flex;
        flex-direction: column; 
        width: 100%;
        margin-bottom: 20px;
        padding: 10px 0;
    }
    /* Fila Superior: Título y Botones alineados horizontalmente */
    .action-bar-row {
    display: flex;
        justify-content: space-between; 
        align-items: center; 
        width: 100%;
        margin-bottom: 15px;
    }
    
    .action-bar div {
        display: flex; 
        gap: 10px;   
        flex-wrap: nowrap; 
    }
    
    .action-bar h2 {
        flex-shrink: 0; 
    }
    
    
    /* Estilo para la tabla */
    .inventory-table {
        width: 100%;
        border-collapse: separate; 
        border-spacing: 0;
        margin-top: 20px;
        border: 1px solid #e0e0e0; 
        border-radius: 8px; 
        overflow: hidden; 
        box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); 
    }
    
    .inventory-table th, .inventory-table td {
        padding: 15px 20px; 
        text-align: left;
        border-bottom: 1px solid #e9ecef; 
    }
    
    .inventory-table th {
        background-color: #f8f9fa; 
        color: #495057; 
        font-weight: 600;
        text-transform: uppercase; 
        font-size: 0.9em;
    }
    
    /* Estilo para la última fila para quitar el borde inferior */
    .inventory-table tbody tr:last-child td {
        border-bottom: none;
    }
    
    /* Efecto Hover  */
    .inventory-table tr:hover {
        background-color: #f1f7ff; 
        transition: background-color 0.3s ease;
    }
    
    /* Estilo para las columnas de Acción y Precio */
    .inventory-table td:nth-last-child(2) { 
        font-weight: 600;
        color: #28a745; 
    }
    
    /* Estilo para los botones pequeños de la tabla */
    .btn-small {
      padding: 5px 10px; 
        font-size: 0.85em;
        border-radius: 3px;
        text-decoration: none;
        display: inline-block;
        margin-right: 5px;
        transition: opacity 0.3s;
    }
    
    .btn-small:hover {
        opacity: 0.8;
    }
    
    .btn-edit {
        background-color: #ffc107; 
        color: #212529;
        border: none;
    }
    
    .btn-delete {
        background-color: #dc3545; 
        color: white;
        border: none;
    }
    
    
    /*Responsive */
    
    /* Estilos para pantallas pequeñas (Móviles) */
    @media (max-width: 768px) {
    	
    	.action-bar-row h2 {
            text-align: center;
            width: 100%;
        }
    	.table-responsive .category-table-responsive td {
        text-align: center; 
    }
    h2 {
            text-align: center; 
        }
    	.table-responsive .category-table-responsive thead tr {
            position: absolute !important; 
            top: -9999px !important;
            left: -9999px !important;
            display: none !important; 
        }
    	
    	.btn-primary, .btn-primary[style] {
            width: 100% !important; 
        }
        .action-bar-row {
            flex-direction: column; 
            align-items: flex-start; 
            gap: 10px; 
        }
        
        /* LOGIN / REGISTRO */
        .wrapper {
            padding: 20px; 
            box-shadow: none; 
            max-width: 90%; 
            margin-top: 20px;
        }
    
        /* HEADER / NAVEGACIÓN */
        .header {
            flex-direction: column; 
            padding: 10px 20px;
            position: static; 
            height: auto;
        }
    
        .header h1 {
            margin-bottom: 10px;
            font-size: 1.2em;
        }
        
        .nav-links {
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 100%;
        }
        
        .nav-links span, .nav-links a {
            margin: 5px 0;
        }
    
        /* CONTENIDO PRINCIPAL */
        .main-content {
            margin-top: 10px; 
            padding: 10px;
        }
    
       /* Estilo del Formulario de Búsqueda */
    .action-buttons-group {
      width: 100%;
            flex-direction: column;
    		gap: 8px;
    }
    /* Contenedor de los botones  */
    .action-bar .action-bar-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        width: 100%; 
        margin-bottom: 15px; 
    }
    
        /*  ESTILOS CRÍTICOS DE LA TABLA (TABLE RESPONSIVE) */
        
        /* HACE QUE CADA FILA Y CELDA SE COMPORTE COMO UN BLOQUE */
        .inventory-table, .inventory-table tbody, .inventory-table tr, .inventory-table td {
            display: block;
            width: 100%;
            box-sizing: border-box; 
        }
    
        /* OCULTA LA CABECERA ORIGINAL (TH) */
        .inventory-table thead {
            display: none;
        }
    
        /* DA ESPACIO Y BORDE A CADA REGISTRO */
        .inventory-table tr {
            margin-bottom: 15px;
            border: 1px solid #dee2e6;
            border-radius: 4px;
            padding: 10px;
        }
    
        /* PREPARA EL ESPACIO PARA LA ETIQUETA */
        .inventory-table td {
            padding-left: 50%; 
            position: relative;
            text-align: right; 
            border: none;
            border-bottom: 1px solid #e9ecef; 
            min-height: 40px; 
        }
        
        /* La última celda de cada fila (Acciones) no debe tener borde inferior */
        .inventory-table tr td:last-child {
            border-bottom: none;
        }
    
        /* MUESTRA LA ETIQUETA USANDO EL ATRIBUTO DATA-LABEL */
        .inventory-table td::before {
            content: attr(data-label); 
            position: absolute;
            left: 15px;
            width: 45%;
            padding-right: 10px;
            white-space: nowrap;
            text-align: left; 
            font-weight: 600;
            color: #495057;
        }
    	/* APILAMIENTO DE LAS DOS COLUMNAS */
        .gestion-categorias-layout {
            flex-direction: column; 
            max-width: none !important; 
            gap: 20px;
        }
        
        /* ANCHO COMPLETO PARA AMBAS COLUMNAS */
        .columna-izquierda, .columna-derecha {
            max-width: none !important;
            width: 100% !important; 
            min-width: auto;
        }
        
        /* CÓDIGO DE TABLA A TARJETA (Ya lo teníamos, solo lo ajustamos al nuevo selector) */
        .category-table-responsive, 
        .category-table-responsive thead, 
        .category-table-responsive tbody, 
        .category-table-responsive th, 
        .category-table-responsive td, 
        .category-table-responsive tr {
            display: block !important; 
        }
    	
        /* BOTONES DE ACCIÓN (para Editar) */
        .category-table-responsive .btn-small { 
            margin-top: 5px;
            display: block; 
            width: 100%;
            text-align: center;
        }
    
    }/* FIN DE LA MEDIA QUERY */
    
    .gestion-categorias-layout {
        max-width: 900px;
        display: flex; 
        gap: 40px;
        justify-content: center;
    }
    
    .columna-izquierda, .columna-derecha {
        flex: 1;
        max-width: 400px;
        min-width: 300px;
    }
    /*  Estilos de Búsqueda y Paginación */
    
    .search-bar {
        margin-bottom: 25px;
        padding: 15px;
        background-color: #f8f9fa;
        border: 1px solid #dee2e6;
        border-radius: 6px;
    }
    
    .btn-search {
        padding: 10px 15px;
        border: none;
        border-radius: 4px;
        background-color: #007bff;
        color: white;
        cursor: pointer;
        text-decoration: none;
        font-weight: 500;
        transition: background-color 0.3s;
    }
    
    .btn-search:hover {
        background-color: #0056b3;
    }
    
    .btn-clear {
        background-color: #6c757d;
    }
    
    .btn-clear:hover {
        background-color: #5a6268;
    }
    
    .search-summary {
        margin-top: -10px;
        margin-bottom: 15px;
        font-size: 1em;
        font-style: italic;
        color: #6c757d;
    }
    
    /* Estilos de la Paginación */
    
    .pagination a {
        display: inline-block; 
        padding: 8px 15px; 
        margin: 0 4px; 
        text-decoration: none; 
        color: #007bff; 
        border: 1px solid #dee2e6; 
        border-radius: 4px;
        background-color: #f8f9fa; 
        transition: all 0.3s ease; 
    	
    }
    .pagination {
        margin-top: 20px;
        margin-bottom: 20px;
        text-align: center; 
        padding: 10px 0;
    }
    
    /* Efecto al pasar el mouse */
    .pagination a:hover {
        background-color: #e9ecef; 
        border-color: #0056b3;
        color: #0056b3;
    }
    
    .page-link {
        padding: 8px 15px;
        border: 1px solid #dee2e6;
        border-radius: 4px;
        text-decoration: none;
        color: #007bff;
        background-color: white;
        transition: background-color 0.3s, color 0.3s;
    }
    
    .page-link:hover {
        background-color: #e9ecef;
    }
    
    .page-link.active {
        background-color: #007bff;
        color: white;
        border-color: #007bff;
        font-weight: 600;
    }
    .pagination a.active {
        background-color: #007bff; 
        color: white; 
        border-color: #007bff;
        cursor: default; 
        pointer-events: none;
    }
    .pagination-count {
        text-align: center;
        font-size: 0.9em;
        color: #6c757d;
        margin-bottom: 20px;
    }
    
    /* NUEVOS ESTILOS PARA SELECT */
    .input-group select {
        width: 100%;
        padding: 10px;
        border: 1px solid #ced4da;
        border-radius: 4px;
        box-sizing: border-box; 
        font-size: 1em;
        height: 40px;
        background-color: white;
        cursor: pointer;
    }
    
    .input-group select:focus {
        border-color: #007bff;
        outline: none;
        box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
    }
    

    Fin 😄



    📥El archivo .rar de descarga incluye todo el proyecto base de Inventario PHP.
    Detalles del Archivo
    Nombre del archivo: Inventariov1.rar
    Tamaño: 21 KB
     Descargar 


    Instrucciones Rápidas
      1. Para poner el sistema en funcionamiento en tu entorno local puedes usar (Laragon, XAMPP, WAMP, MAMP, etc.)
      2. Descomprimir y colocar la carpeta del proyecto en el directorio raíz de tu servidor: Si usas Laragon colócalo dentro de la carpeta www. Si usas XAMPP colócalo dentro de la carpeta htdocs
      3. Abre tu gestor de bases de datos (phpMyAdmin, MySQL,etc) e importa el archivo SQL o simplemente copia y pega la consulta SQL completa en la ventana de comandos de tu gestor y ejecútala.
      4. Revisa y confirma que las credenciales en el archivo conexion.php sean exactas (nombre de la base de datos, usuario y contraseña) a las de tu servidor local, abre tu navegador y navega a la URL local de tu proyecto: localhost/nombre_de_tu_carpeta/ o localhost. 👍😸