Spaces:
Running
Running
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Sistema de Inventario</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .sidebar { | |
| transition: all 0.3s; | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| transform: translateX(-100%); | |
| } | |
| .sidebar.active { | |
| transform: translateX(0); | |
| } | |
| } | |
| .product-card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 font-sans"> | |
| <!-- Login Screen --> | |
| <div id="login-screen" class="fixed inset-0 flex items-center justify-center bg-gray-900 bg-opacity-75 z-50"> | |
| <div class="bg-white rounded-lg shadow-xl p-8 w-full max-w-md"> | |
| <div class="text-center mb-8"> | |
| <i class="fas fa-boxes text-4xl text-blue-600 mb-4"></i> | |
| <h1 class="text-3xl font-bold text-gray-800">Sistema de Inventario</h1> | |
| <p class="text-gray-600 mt-2">Ingrese sus credenciales para acceder</p> | |
| </div> | |
| <form id="login-form" class="space-y-6"> | |
| <div> | |
| <label for="username" class="block text-sm font-medium text-gray-700 mb-1">Usuario</label> | |
| <input type="text" id="username" name="username" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="password" class="block text-sm font-medium text-gray-700 mb-1">Contraseña</label> | |
| <input type="password" id="password" name="password" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <button type="submit" | |
| class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 transition duration-300 flex items-center justify-center"> | |
| <i class="fas fa-sign-in-alt mr-2"></i> Iniciar Sesión | |
| </button> | |
| </div> | |
| <div class="text-center text-sm text-gray-500"> | |
| <p>Usuario demo: admin / pass123 (rol admin)</p> | |
| <p>Usuario demo: user / pass123 (rol usuario)</p> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Main App (hidden until login) --> | |
| <div id="app-container" class="hidden"> | |
| <!-- Sidebar --> | |
| <div class="sidebar fixed inset-y-0 left-0 bg-gray-800 text-white w-64 z-40"> | |
| <div class="p-4 flex items-center border-b border-gray-700"> | |
| <i class="fas fa-boxes text-2xl text-blue-400 mr-3"></i> | |
| <h1 class="text-xl font-bold">Inventario</h1> | |
| <button id="close-sidebar" class="ml-auto md:hidden"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <nav class="p-4"> | |
| <div class="mb-8"> | |
| <p class="text-gray-400 uppercase text-xs font-bold mb-4">Menú Principal</p> | |
| <a href="#" class="flex items-center py-2 px-3 bg-gray-700 rounded-md text-white mb-2"> | |
| <i class="fas fa-tachometer-alt mr-3"></i> Dashboard | |
| </a> | |
| <a href="#" id="nav-products" class="flex items-center py-2 px-3 hover:bg-gray-700 rounded-md text-gray-300 hover:text-white mb-2"> | |
| <i class="fas fa-boxes mr-3"></i> Productos | |
| </a> | |
| <a href="#" id="nav-add-product" class="flex items-center py-2 px-3 hover:bg-gray-700 rounded-md text-gray-300 hover:text-white mb-2"> | |
| <i class="fas fa-plus-circle mr-3"></i> Agregar Producto | |
| </a> | |
| </div> | |
| <div> | |
| <p class="text-gray-400 uppercase text-xs font-bold mb-4">Usuario</p> | |
| <a href="#" id="logout-btn" class="flex items-center py-2 px-3 hover:bg-gray-700 rounded-md text-gray-300 hover:text-white"> | |
| <i class="fas fa-sign-out-alt mr-3"></i> Cerrar Sesión | |
| </a> | |
| </div> | |
| </nav> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="ml-0 md:ml-64 transition-all duration-300"> | |
| <!-- Header --> | |
| <header class="bg-white shadow-sm py-4 px-6 flex items-center justify-between sticky top-0 z-30"> | |
| <button id="menu-toggle" class="md:hidden text-gray-600"> | |
| <i class="fas fa-bars text-xl"></i> | |
| </button> | |
| <div class="flex items-center"> | |
| <div class="mr-4"> | |
| <span id="user-role-badge" class="px-3 py-1 rounded-full text-xs font-semibold"></span> | |
| </div> | |
| <div class="relative"> | |
| <button id="user-menu-btn" class="flex items-center focus:outline-none"> | |
| <span id="username-display" class="mr-2 font-medium"></span> | |
| <i class="fas fa-user-circle text-2xl text-gray-600"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Dashboard Content --> | |
| <main class="p-6"> | |
| <!-- Dashboard Overview --> | |
| <div id="dashboard-view"> | |
| <div class="mb-8"> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6">Resumen del Inventario</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4"> | |
| <i class="fas fa-boxes text-xl"></i> | |
| </div> | |
| <div> | |
| <p class="text-gray-500 text-sm">Productos totales</p> | |
| <h3 class="text-2xl font-bold" id="total-products">0</h3> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-green-100 text-green-600 mr-4"> | |
| <i class="fas fa-box-open text-xl"></i> | |
| </div> | |
| <div> | |
| <p class="text-gray-500 text-sm">Stock disponible</p> | |
| <h3 class="text-2xl font-bold" id="total-stock">0</h3> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4"> | |
| <i class="fas fa-tags text-xl"></i> | |
| </div> | |
| <div> | |
| <p class="text-gray-500 text-sm">Valor total</p> | |
| <h3 class="text-2xl font-bold" id="total-value">$0</h3> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h2 class="text-2xl font-bold text-gray-800 mb-6">Productos con bajo stock</h2> | |
| <div class="bg-white rounded-lg shadow overflow-hidden"> | |
| <div id="low-stock-products" class="divide-y divide-gray-200"> | |
| <!-- Low stock products will be loaded here --> | |
| <div class="p-4 text-center text-gray-500"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Cargando productos... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Products List View --> | |
| <div id="products-view" class="hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800">Lista de Productos</h2> | |
| <button id="add-product-btn" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-300 flex items-center"> | |
| <i class="fas fa-plus mr-2"></i> Agregar Producto | |
| </button> | |
| </div> | |
| <div class="bg-white rounded-lg shadow overflow-hidden"> | |
| <div class="overflow-x-auto"> | |
| <table class="min-w-full divide-y divide-gray-200"> | |
| <thead class="bg-gray-50"> | |
| <tr> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nombre</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Categoría</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Precio</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cantidad</th> | |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th> | |
| </tr> | |
| </thead> | |
| <tbody id="products-table-body" class="bg-white divide-y divide-gray-200"> | |
| <!-- Products will be loaded here --> | |
| <tr> | |
| <td colspan="5" class="px-6 py-4 text-center text-gray-500"> | |
| <i class="fas fa-spinner fa-spin mr-2"></i> Cargando productos... | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Add/Edit Product Form --> | |
| <div id="product-form-view" class="hidden"> | |
| <div class="mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-800" id="form-title">Agregar Nuevo Producto</h2> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <form id="product-form" class="space-y-6"> | |
| <input type="hidden" id="product-id"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label for="product-name" class="block text-sm font-medium text-gray-700 mb-1">Nombre del Producto *</label> | |
| <input type="text" id="product-name" name="product-name" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| <div> | |
| <label for="product-category" class="block text-sm font-medium text-gray-700 mb-1">Categoría *</label> | |
| <select id="product-category" name="product-category" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <option value="">Seleccione una categoría</option> | |
| <option value="Electrónicos">Electrónicos</option> | |
| <option value="Ropa">Ropa</option> | |
| <option value="Alimentos">Alimentos</option> | |
| <option value="Hogar">Hogar</option> | |
| <option value="Oficina">Oficina</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="product-price" class="block text-sm font-medium text-gray-700 mb-1">Precio *</label> | |
| <div class="relative"> | |
| <span class="absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500">$</span> | |
| <input type="number" id="product-price" name="product-price" step="0.01" min="0" required | |
| class="w-full pl-8 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="product-quantity" class="block text-sm font-medium text-gray-700 mb-1">Cantidad *</label> | |
| <input type="number" id="product-quantity" name="product-quantity" min="0" required | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| </div> | |
| <div> | |
| <label for="product-description" class="block text-sm font-medium text-gray-700 mb-1">Descripción</label> | |
| <textarea id="product-description" name="product-description" rows="3" | |
| class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea> | |
| </div> | |
| <div class="flex justify-end space-x-4"> | |
| <button type="button" id="cancel-form-btn" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 transition duration-300"> | |
| Cancelar | |
| </button> | |
| <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-300"> | |
| Guardar Producto | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Edit Product Modal --> | |
| <div id="edit-product-modal" class="fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-bold text-gray-800">Editar Producto</h3> | |
| <button id="close-edit-modal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <form id="edit-product-form" class="space-y-4"> | |
| <input type="hidden" id="edit-product-id"> | |
| <div> | |
| <label for="edit-product-name" class="block text-sm font-medium text-gray-700 mb-1">Nombre</label> | |
| <input type="text" id="edit-product-name" class="w-full px-4 py-2 border border-gray-300 rounded-md"> | |
| </div> | |
| <div> | |
| <label for="edit-product-price" class="block text-sm font-medium text-gray-700 mb-1">Precio</label> | |
| <input type="number" id="edit-product-price" step="0.01" class="w-full px-4 py-2 border border-gray-300 rounded-md"> | |
| </div> | |
| <div> | |
| <label for="edit-product-quantity" class="block text-sm font-medium text-gray-700 mb-1">Cantidad</label> | |
| <input type="number" id="edit-product-quantity" class="w-full px-4 py-2 border border-gray-300 rounded-md"> | |
| </div> | |
| <div class="flex justify-end space-x-3 pt-4"> | |
| <button type="button" id="cancel-edit-btn" class="px-4 py-2 border border-gray-300 rounded-md"> | |
| Cancelar | |
| </button> | |
| <button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded-md"> | |
| Guardar Cambios | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Delete Confirmation Modal --> | |
| <div id="delete-modal" class="fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-bold text-gray-800">Confirmar Eliminación</h3> | |
| <button id="close-delete-modal" class="text-gray-500 hover:text-gray-700"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <p class="text-gray-700 mb-6">¿Estás seguro de que deseas eliminar este producto? Esta acción no se puede deshacer.</p> | |
| <div class="flex justify-end space-x-3"> | |
| <button type="button" id="cancel-delete-btn" class="px-4 py-2 border border-gray-300 rounded-md"> | |
| Cancelar | |
| </button> | |
| <button type="button" id="confirm-delete-btn" class="bg-red-600 text-white px-4 py-2 rounded-md"> | |
| Eliminar | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Mock database | |
| const mockDatabase = { | |
| users: [ | |
| { id: 1, username: 'admin', password: 'pass123', role: 'admin', name: 'Administrador' }, | |
| { id: 2, username: 'user', password: 'pass123', role: 'user', name: 'Usuario Regular' } | |
| ], | |
| products: [ | |
| { id: 1, name: 'Laptop HP', category: 'Electrónicos', price: 1200, quantity: 15, description: 'Laptop HP con 16GB RAM y 512GB SSD' }, | |
| { id: 2, name: 'Smartphone Samsung', category: 'Electrónicos', price: 800, quantity: 25, description: 'Smartphone Samsung Galaxy S21' }, | |
| { id: 3, name: 'Camisa de algodón', category: 'Ropa', price: 25, quantity: 50, description: 'Camisa 100% algodón talla M' }, | |
| { id: 4, name: 'Arroz 5kg', category: 'Alimentos', price: 10, quantity: 40, description: 'Arroz blanco grano largo 5kg' }, | |
| { id: 5, name: 'Silla de oficina', category: 'Oficina', price: 150, quantity: 10, description: 'Silla ergonómica para oficina' }, | |
| { id: 6, name: 'Monitor 24"', category: 'Electrónicos', price: 180, quantity: 2, description: 'Monitor LED 24 pulgadas Full HD' }, | |
| { id: 7, name: 'Juego de sábanas', category: 'Hogar', price: 45, quantity: 15, description: 'Juego de sábanas de algodón king size' } | |
| ] | |
| }; | |
| // App state | |
| const state = { | |
| currentUser: null, | |
| currentView: 'dashboard', | |
| products: [], | |
| productToDelete: null, | |
| productToEdit: null | |
| }; | |
| // DOM Elements | |
| const loginScreen = document.getElementById('login-screen'); | |
| const appContainer = document.getElementById('app-container'); | |
| const loginForm = document.getElementById('login-form'); | |
| const usernameInput = document.getElementById('username'); | |
| const passwordInput = document.getElementById('password'); | |
| const logoutBtn = document.getElementById('logout-btn'); | |
| const usernameDisplay = document.getElementById('username-display'); | |
| const userRoleBadge = document.getElementById('user-role-badge'); | |
| const menuToggle = document.getElementById('menu-toggle'); | |
| const closeSidebar = document.getElementById('close-sidebar'); | |
| const sidebar = document.querySelector('.sidebar'); | |
| const navProducts = document.getElementById('nav-products'); | |
| const navAddProduct = document.getElementById('nav-add-product'); | |
| const dashboardView = document.getElementById('dashboard-view'); | |
| const productsView = document.getElementById('products-view'); | |
| const productFormView = document.getElementById('product-form-view'); | |
| const productsTableBody = document.getElementById('products-table-body'); | |
| const addProductBtn = document.getElementById('add-product-btn'); | |
| const productForm = document.getElementById('product-form'); | |
| const productIdInput = document.getElementById('product-id'); | |
| const productNameInput = document.getElementById('product-name'); | |
| const productCategoryInput = document.getElementById('product-category'); | |
| const productPriceInput = document.getElementById('product-price'); | |
| const productQuantityInput = document.getElementById('product-quantity'); | |
| const productDescriptionInput = document.getElementById('product-description'); | |
| const cancelFormBtn = document.getElementById('cancel-form-btn'); | |
| const formTitle = document.getElementById('form-title'); | |
| const totalProducts = document.getElementById('total-products'); | |
| const totalStock = document.getElementById('total-stock'); | |
| const totalValue = document.getElementById('total-value'); | |
| const lowStockProducts = document.getElementById('low-stock-products'); | |
| const editProductModal = document.getElementById('edit-product-modal'); | |
| const closeEditModal = document.getElementById('close-edit-modal'); | |
| const cancelEditBtn = document.getElementById('cancel-edit-btn'); | |
| const editProductForm = document.getElementById('edit-product-form'); | |
| const editProductId = document.getElementById('edit-product-id'); | |
| const editProductName = document.getElementById('edit-product-name'); | |
| const editProductPrice = document.getElementById('edit-product-price'); | |
| const editProductQuantity = document.getElementById('edit-product-quantity'); | |
| const deleteModal = document.getElementById('delete-modal'); | |
| const closeDeleteModal = document.getElementById('close-delete-modal'); | |
| const cancelDeleteBtn = document.getElementById('cancel-delete-btn'); | |
| const confirmDeleteBtn = document.getElementById('confirm-delete-btn'); | |
| // Event Listeners | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Initialize mock data | |
| state.products = [...mockDatabase.products]; | |
| // Check if user is already logged in (for demo purposes) | |
| const loggedInUser = localStorage.getItem('inventoryUser'); | |
| if (loggedInUser) { | |
| state.currentUser = JSON.parse(loggedInUser); | |
| handleSuccessfulLogin(); | |
| } | |
| }); | |
| loginForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const username = usernameInput.value.trim(); | |
| const password = passwordInput.value.trim(); | |
| // Validate credentials | |
| const user = mockDatabase.users.find(u => u.username === username && u.password === password); | |
| if (user) { | |
| state.currentUser = user; | |
| localStorage.setItem('inventoryUser', JSON.stringify(user)); | |
| handleSuccessfulLogin(); | |
| } else { | |
| alert('Credenciales incorrectas. Intente nuevamente.'); | |
| } | |
| }); | |
| logoutBtn.addEventListener('click', () => { | |
| state.currentUser = null; | |
| localStorage.removeItem('inventoryUser'); | |
| loginScreen.classList.remove('hidden'); | |
| appContainer.classList.add('hidden'); | |
| usernameInput.value = ''; | |
| passwordInput.value = ''; | |
| }); | |
| menuToggle.addEventListener('click', () => { | |
| sidebar.classList.add('active'); | |
| }); | |
| closeSidebar.addEventListener('click', () => { | |
| sidebar.classList.remove('active'); | |
| }); | |
| navProducts.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| showProductsView(); | |
| }); | |
| navAddProduct.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| showAddProductForm(); | |
| }); | |
| addProductBtn.addEventListener('click', () => { | |
| showAddProductForm(); | |
| }); | |
| productForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| const productData = { | |
| name: productNameInput.value.trim(), | |
| category: productCategoryInput.value, | |
| price: parseFloat(productPriceInput.value), | |
| quantity: parseInt(productQuantityInput.value), | |
| description: productDescriptionInput.value.trim() | |
| }; | |
| if (productIdInput.value) { | |
| // Edit existing product | |
| const productId = parseInt(productIdInput.value); | |
| const productIndex = state.products.findIndex(p => p.id === productId); | |
| if (productIndex !== -1) { | |
| state.products[productIndex] = { | |
| ...state.products[productIndex], | |
| ...productData | |
| }; | |
| alert('Producto actualizado correctamente'); | |
| } | |
| } else { | |
| // Add new product | |
| const newProduct = { | |
| id: state.products.length > 0 ? Math.max(...state.products.map(p => p.id)) + 1 : 1, | |
| ...productData | |
| }; | |
| state.products.push(newProduct); | |
| alert('Producto agregado correctamente'); | |
| } | |
| // Reset form and show products view | |
| productForm.reset(); | |
| showProductsView(); | |
| updateDashboardStats(); | |
| }); | |
| cancelFormBtn.addEventListener('click', () => { | |
| productForm.reset(); | |
| showProductsView(); | |
| }); | |
| // Handle edit product modal | |
| document.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('edit-product-btn')) { | |
| const productId = parseInt(e.target.dataset.id); | |
| const product = state.products.find(p => p.id === productId); | |
| if (product) { | |
| state.productToEdit = product; | |
| showEditProductModal(product); | |
| } | |
| } | |
| if (e.target.classList.contains('delete-product-btn')) { | |
| const productId = parseInt(e.target.dataset.id); | |
| const product = state.products.find(p => p.id === productId); | |
| if (product) { | |
| state.productToDelete = product; | |
| showDeleteModal(); | |
| } | |
| } | |
| }); | |
| editProductForm.addEventListener('submit', (e) => { | |
| e.preventDefault(); | |
| if (state.productToEdit) { | |
| const productIndex = state.products.findIndex(p => p.id === state.productToEdit.id); | |
| if (productIndex !== -1) { | |
| state.products[productIndex] = { | |
| ...state.products[productIndex], | |
| name: editProductName.value.trim(), | |
| price: parseFloat(editProductPrice.value), | |
| quantity: parseInt(editProductQuantity.value) | |
| }; | |
| alert('Producto actualizado correctamente'); | |
| hideEditProductModal(); | |
| showProductsView(); | |
| updateDashboardStats(); | |
| } | |
| } | |
| }); | |
| closeEditModal.addEventListener('click', hideEditProductModal); | |
| cancelEditBtn.addEventListener('click', hideEditProductModal); | |
| confirmDeleteBtn.addEventListener('click', () => { | |
| if (state.productToDelete) { | |
| state.products = state.products.filter(p => p.id !== state.productToDelete.id); | |
| alert('Producto eliminado correctamente'); | |
| hideDeleteModal(); | |
| showProductsView(); | |
| updateDashboardStats(); | |
| } | |
| }); | |
| closeDeleteModal.addEventListener('click', hideDeleteModal); | |
| cancelDeleteBtn.addEventListener('click', hideDeleteModal); | |
| // Functions | |
| function handleSuccessfulLogin() { | |
| loginScreen.classList.add('hidden'); | |
| appContainer.classList.remove('hidden'); | |
| // Update UI with user info | |
| usernameDisplay.textContent = state.currentUser.name; | |
| if (state.currentUser.role === 'admin') { | |
| userRoleBadge.textContent = 'Administrador'; | |
| userRoleBadge.classList.add('bg-purple-100', 'text-purple-800'); | |
| } else { | |
| userRoleBadge.textContent = 'Usuario'; | |
| userRoleBadge.classList.add('bg-blue-100', 'text-blue-800'); | |
| } | |
| // Load initial view | |
| showDashboardView(); | |
| updateDashboardStats(); | |
| } | |
| function showDashboardView() { | |
| state.currentView = 'dashboard'; | |
| dashboardView.classList.remove('hidden'); | |
| productsView.classList.add('hidden'); | |
| productFormView.classList.add('hidden'); | |
| // Update active nav item | |
| document.querySelectorAll('nav a').forEach(link => { | |
| link.classList.remove('bg-gray-700', 'text-white'); | |
| link.classList.add('text-gray-300', 'hover:bg-gray-700', 'hover:text-white'); | |
| }); | |
| document.querySelector('nav a:first-child').classList.add('bg-gray-700', 'text-white'); | |
| document.querySelector('nav a:first-child').classList.remove('text-gray-300', 'hover:bg-gray-700', 'hover:text-white'); | |
| } | |
| function showProductsView() { | |
| state.currentView = 'products'; | |
| dashboardView.classList.add('hidden'); | |
| productsView.classList.remove('hidden'); | |
| productFormView.classList.add('hidden'); | |
| // Update active nav item | |
| document.querySelectorAll('nav a').forEach(link => { | |
| link.classList.remove('bg-gray-700', 'text-white'); | |
| link.classList.add('text-gray-300', 'hover:bg-gray-700', 'hover:text-white'); | |
| }); | |
| navProducts.classList.add('bg-gray-700', 'text-white'); | |
| navProducts.classList.remove('text-gray-300', 'hover:bg-gray-700', 'hover:text-white'); | |
| // Render products table | |
| renderProductsTable(); | |
| } | |
| function showAddProductForm() { | |
| state.currentView = 'add-product'; | |
| dashboardView.classList.add('hidden'); | |
| productsView.classList.add('hidden'); | |
| productFormView.classList.remove('hidden'); | |
| // Update form title and reset | |
| formTitle.textContent = 'Agregar Nuevo Producto'; | |
| productForm.reset(); | |
| productIdInput.value = ''; | |
| // Update active nav item | |
| document.querySelectorAll('nav a').forEach(link => { | |
| link.classList.remove('bg-gray-700', 'text-white'); | |
| link.classList.add('text-gray-300', 'hover:bg-gray-700', 'hover:text-white'); | |
| }); | |
| navAddProduct.classList.add('bg-gray-700', 'text-white'); | |
| navAddProduct.classList.remove('text-gray-300', 'hover:bg-gray-700', 'hover:text-white'); | |
| } | |
| function showEditProductForm(product) { | |
| state.currentView = 'edit-product'; | |
| dashboardView.classList.add('hidden'); | |
| productsView.classList.add('hidden'); | |
| productFormView.classList.remove('hidden'); | |
| // Update form title and fill with product data | |
| formTitle.textContent = 'Editar Producto'; | |
| productIdInput.value = product.id; | |
| productNameInput.value = product.name; | |
| productCategoryInput.value = product.category; | |
| productPriceInput.value = product.price; | |
| productQuantityInput.value = product.quantity; | |
| productDescriptionInput.value = product.description || ''; | |
| } | |
| function renderProductsTable() { | |
| if (state.products.length === 0) { | |
| productsTableBody.innerHTML = ` | |
| <tr> | |
| <td colspan="5" class="px-6 py-4 text-center text-gray-500"> | |
| No hay productos registrados | |
| </td> | |
| </tr> | |
| `; | |
| return; | |
| } | |
| let html = ''; | |
| state.products.forEach(product => { | |
| const lowStockClass = product.quantity < 5 ? 'text-red-600 font-semibold' : ''; | |
| html += ` | |
| <tr class="hover:bg-gray-50"> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="text-sm font-medium text-gray-900">${product.name}</div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm text-gray-500">${product.category}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm text-gray-900">$${product.price.toFixed(2)}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm ${lowStockClass}">${product.quantity}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | |
| ${state.currentUser.role === 'admin' ? ` | |
| <button class="edit-product-btn text-blue-600 hover:text-blue-900 mr-4" data-id="${product.id}"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| <button class="delete-product-btn text-red-600 hover:text-red-900" data-id="${product.id}"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| ` : ''} | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| productsTableBody.innerHTML = html; | |
| } | |
| function updateDashboardStats() { | |
| // Update total products | |
| totalProducts.textContent = state.products.length; | |
| // Update total stock | |
| const totalStockCount = state.products.reduce((sum, product) => sum + product.quantity, 0); | |
| totalStock.textContent = totalStockCount; | |
| // Update total value | |
| const totalValueAmount = state.products.reduce((sum, product) => sum + (product.price * product.quantity), 0); | |
| totalValue.textContent = `$${totalValueAmount.toFixed(2)}`; | |
| // Update low stock products | |
| const lowStockItems = state.products.filter(p => p.quantity < 5); | |
| if (lowStockItems.length === 0) { | |
| lowStockProducts.innerHTML = ` | |
| <div class="p-4 text-center text-gray-500"> | |
| No hay productos con bajo stock | |
| </div> | |
| `; | |
| } else { | |
| let html = ''; | |
| lowStockItems.forEach(product => { | |
| html += ` | |
| <div class="p-4 flex items-center justify-between hover:bg-gray-50"> | |
| <div> | |
| <h4 class="font-medium text-gray-900">${product.name}</h4> | |
| <p class="text-sm text-gray-500">${product.category}</p> | |
| </div> | |
| <div class="text-red-600 font-semibold"> | |
| ${product.quantity} en stock | |
| </div> | |
| </div> | |
| `; | |
| }); | |
| lowStockProducts.innerHTML = html; | |
| } | |
| } | |
| function showEditProductModal(product) { | |
| editProductId.value = product.id; | |
| editProductName.value = product.name; | |
| editProductPrice.value = product.price; | |
| editProductQuantity.value = product.quantity; | |
| editProductModal.classList.remove('hidden'); | |
| } | |
| function hideEditProductModal() { | |
| editProductModal.classList.add('hidden'); | |
| state.productToEdit = null; | |
| } | |
| function showDeleteModal() { | |
| deleteModal.classList.remove('hidden'); | |
| } | |
| function hideDeleteModal() { | |
| deleteModal.classList.add('hidden'); | |
| state.productToDelete = null; | |
| } | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=fakesisalg/test" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |