Spaces:
Running
Running
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Inventory Manager</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> | |
| /* Custom styles that can't be achieved with Tailwind */ | |
| .sidebar { | |
| transition: all 0.3s ease; | |
| } | |
| .sidebar.collapsed { | |
| width: 70px; | |
| } | |
| .sidebar.collapsed .nav-text { | |
| display: none; | |
| } | |
| .sidebar.collapsed .logo-text { | |
| display: none; | |
| } | |
| .sidebar.collapsed .nav-item { | |
| justify-content: center; | |
| } | |
| .content { | |
| transition: all 0.3s ease; | |
| } | |
| .content.expanded { | |
| margin-left: 70px; | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| width: 70px; | |
| } | |
| .sidebar .nav-text { | |
| display: none; | |
| } | |
| .sidebar .logo-text { | |
| display: none; | |
| } | |
| .sidebar .nav-item { | |
| justify-content: center; | |
| } | |
| .content { | |
| margin-left: 70px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100"> | |
| <!-- Login Screen (visible by default) --> | |
| <div id="login-screen" class="fixed inset-0 flex items-center justify-center bg-gray-900 bg-opacity-90 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-5xl text-blue-600 mb-4"></i> | |
| <h1 class="text-3xl font-bold text-gray-800">Inventory Manager</h1> | |
| <p class="text-gray-600">Inicia sesión para acceder al sistema</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 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> | |
| Iniciar Sesión | |
| </button> | |
| </div> | |
| <div id="login-error" class="text-red-500 text-sm hidden"> | |
| <i class="fas fa-exclamation-circle mr-1"></i> Usuario o contraseña incorrectos | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| <!-- Dashboard (hidden by default) --> | |
| <div id="dashboard" class="hidden"> | |
| <!-- Sidebar --> | |
| <div id="sidebar" class="sidebar fixed inset-y-0 left-0 bg-gray-800 text-white w-64 shadow-lg"> | |
| <div class="flex items-center justify-between p-4 border-b border-gray-700"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-boxes text-2xl text-blue-400"></i> | |
| <span class="logo-text ml-3 text-xl font-semibold">Inventory</span> | |
| </div> | |
| <button id="toggle-sidebar" class="text-gray-400 hover:text-white focus:outline-none"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| </div> | |
| <nav class="mt-6"> | |
| <div class="px-4 space-y-2"> | |
| <a href="#" class="nav-item flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 rounded-md transition duration-300 active-nav" data-section="dashboard-section"> | |
| <i class="fas fa-tachometer-alt"></i> | |
| <span class="nav-text ml-3">Resumen</span> | |
| </a> | |
| <a href="#" class="nav-item flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 rounded-md transition duration-300" data-section="products-section"> | |
| <i class="fas fa-box-open"></i> | |
| <span class="nav-text ml-3">Productos</span> | |
| </a> | |
| <a href="#" class="nav-item flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 rounded-md transition duration-300" data-section="add-product-section"> | |
| <i class="fas fa-plus-circle"></i> | |
| <span class="nav-text ml-3">Agregar Producto</span> | |
| </a> | |
| <a href="#" id="logout-btn" class="nav-item flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 rounded-md transition duration-300"> | |
| <i class="fas fa-sign-out-alt"></i> | |
| <span class="nav-text ml-3">Cerrar Sesión</span> | |
| </a> | |
| </div> | |
| </nav> | |
| </div> | |
| <!-- Main Content --> | |
| <div id="content" class="content ml-64 min-h-screen transition-all duration-300"> | |
| <!-- Top Navigation --> | |
| <header class="bg-white shadow-sm"> | |
| <div class="flex justify-between items-center px-6 py-4"> | |
| <h1 class="text-2xl font-semibold text-gray-800" id="section-title">Resumen</h1> | |
| <div class="flex items-center space-x-4"> | |
| <div class="relative"> | |
| <button id="user-menu-btn" class="flex items-center focus:outline-none"> | |
| <div class="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center text-white"> | |
| <span id="user-initials">AD</span> | |
| </div> | |
| <span class="ml-2 text-gray-700 hidden md:inline" id="username-display">Admin</span> | |
| <i class="fas fa-chevron-down ml-1 text-gray-500 hidden md:inline"></i> | |
| </button> | |
| <div id="user-menu" class="hidden absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10"> | |
| <div class="px-4 py-2 text-sm text-gray-700 border-b"> | |
| <div>Conectado como</div> | |
| <div class="font-medium" id="menu-username">admin</div> | |
| </div> | |
| <a href="#" id="menu-logout-btn" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"> | |
| <i class="fas fa-sign-out-alt mr-2"></i>Cerrar sesión | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Dashboard Sections --> | |
| <main class="p-6"> | |
| <!-- Dashboard Summary Section --> | |
| <section id="dashboard-section" class="section-content"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-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"> | |
| <i class="fas fa-boxes text-xl"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <p class="text-sm font-medium text-gray-500">Total Productos</p> | |
| <p class="text-2xl font-semibold text-gray-800" id="total-products">0</p> | |
| </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"> | |
| <i class="fas fa-check-circle text-xl"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <p class="text-sm font-medium text-gray-500">Disponibles</p> | |
| <p class="text-2xl font-semibold text-gray-800" id="available-products">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-yellow-100 text-yellow-600"> | |
| <i class="fas fa-exclamation-triangle text-xl"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <p class="text-sm font-medium text-gray-500">Bajo Stock</p> | |
| <p class="text-2xl font-semibold text-gray-800" id="low-stock-products">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex items-center"> | |
| <div class="p-3 rounded-full bg-red-100 text-red-600"> | |
| <i class="fas fa-times-circle text-xl"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <p class="text-sm font-medium text-gray-500">Agotados</p> | |
| <p class="text-2xl font-semibold text-gray-800" id="out-of-stock-products">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6 mb-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-semibold text-gray-800">Productos por Categoría</h2> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4" id="categories-chart"> | |
| <!-- Categories will be populated here --> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h2 class="text-lg font-semibold text-gray-800">Productos Recientes</h2> | |
| <a href="#" class="text-blue-600 hover:text-blue-800 text-sm font-medium" data-section="products-section">Ver todos</a> | |
| </div> | |
| <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> | |
| </tr> | |
| </thead> | |
| <tbody class="bg-white divide-y divide-gray-200" id="recent-products"> | |
| <!-- Recent products will be populated here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Products List Section --> | |
| <section id="products-section" class="section-content hidden"> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h2 class="text-xl font-semibold text-gray-800">Lista de Productos</h2> | |
| <div class="flex space-x-3"> | |
| <div class="relative"> | |
| <input type="text" id="product-search" placeholder="Buscar productos..." class="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> | |
| </div> | |
| <button id="refresh-products" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-300"> | |
| <i class="fas fa-sync-alt"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <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 class="bg-white divide-y divide-gray-200" id="products-table"> | |
| <!-- Products will be populated here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="mt-4 flex justify-between items-center"> | |
| <div class="text-sm text-gray-500"> | |
| Mostrando <span id="showing-from">1</span> a <span id="showing-to">10</span> de <span id="total-items">0</span> productos | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="prev-page" class="px-3 py-1 border border-gray-300 rounded-md bg-white text-gray-700 disabled:opacity-50" disabled> | |
| <i class="fas fa-chevron-left"></i> | |
| </button> | |
| <button id="next-page" class="px-3 py-1 border border-gray-300 rounded-md bg-white text-gray-700 disabled:opacity-50" disabled> | |
| <i class="fas fa-chevron-right"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Add Product Section --> | |
| <section id="add-product-section" class="section-content hidden"> | |
| <div class="bg-white rounded-lg shadow p-6"> | |
| <h2 class="text-xl font-semibold text-gray-800 mb-6">Agregar Nuevo Producto</h2> | |
| <form id="add-product-form" class="space-y-6"> | |
| <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="Oficina">Oficina</option> | |
| <option value="Hogar">Hogar</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 left-3 top-3 text-gray-500">$</span> | |
| <input type="number" id="product-price" name="product-price" min="0" step="0.01" required | |
| class="w-full pl-8 pr-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="reset" class="px-4 py-2 border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-gray-50 transition duration-300"> | |
| Limpiar | |
| </button> | |
| <button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> | |
| Guardar Producto | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </section> | |
| <!-- Edit Product Modal (hidden by default) --> | |
| <div id="edit-product-modal" class="fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-2xl"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-semibold 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-6"> | |
| <input type="hidden" id="edit-product-id"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label for="edit-product-name" class="block text-sm font-medium text-gray-700 mb-1">Nombre del Producto *</label> | |
| <input type="text" id="edit-product-name" name="edit-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="edit-product-category" class="block text-sm font-medium text-gray-700 mb-1">Categoría *</label> | |
| <select id="edit-product-category" name="edit-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="Oficina">Oficina</option> | |
| <option value="Hogar">Hogar</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label for="edit-product-price" class="block text-sm font-medium text-gray-700 mb-1">Precio *</label> | |
| <div class="relative"> | |
| <span class="absolute left-3 top-3 text-gray-500">$</span> | |
| <input type="number" id="edit-product-price" name="edit-product-price" min="0" step="0.01" required | |
| class="w-full pl-8 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> | |
| </div> | |
| </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" name="edit-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="edit-product-description" class="block text-sm font-medium text-gray-700 mb-1">Descripción</label> | |
| <textarea id="edit-product-description" name="edit-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-edit" class="px-4 py-2 border border-gray-300 rounded-md bg-white text-gray-700 hover:bg-gray-50 transition duration-300"> | |
| Cancelar | |
| </button> | |
| <button type="submit" class="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> | |
| Guardar Cambios | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <!-- Alert Notification (hidden by default) --> | |
| <div id="alert-notification" class="fixed bottom-4 right-4 z-50 hidden"> | |
| <div class="bg-white rounded-lg shadow-lg overflow-hidden w-80"> | |
| <div class="flex items-center px-4 py-3 border-l-4 border-green-500"> | |
| <div class="text-green-500 mr-3"> | |
| <i class="fas fa-check-circle text-xl"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-medium text-gray-800" id="alert-title">Éxito</h4> | |
| <p class="text-sm text-gray-600" id="alert-message">Operación realizada con éxito</p> | |
| </div> | |
| <button id="close-alert" class="ml-auto text-gray-400 hover:text-gray-500"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration variables | |
| // IMPORTANT: Configure these values according to your backend API | |
| const config = { | |
| // Base URL of your API (Spring Boot backend) | |
| baseUrl: 'http://localhost:8080/api', | |
| // Endpoints | |
| endpoints: { | |
| login: '/auth/login', | |
| products: '/productos' | |
| }, | |
| // Default pagination settings | |
| pagination: { | |
| pageSize: 10, | |
| currentPage: 1 | |
| } | |
| }; | |
| // Application state | |
| const state = { | |
| currentUser: null, | |
| products: [], | |
| filteredProducts: [], | |
| currentSection: 'dashboard-section', | |
| isAdmin: false | |
| }; | |
| // DOM Elements | |
| const elements = { | |
| loginScreen: document.getElementById('login-screen'), | |
| dashboard: document.getElementById('dashboard'), | |
| loginForm: document.getElementById('login-form'), | |
| loginError: document.getElementById('login-error'), | |
| sidebar: document.getElementById('sidebar'), | |
| toggleSidebar: document.getElementById('toggle-sidebar'), | |
| content: document.getElementById('content'), | |
| sectionTitle: document.getElementById('section-title'), | |
| usernameDisplay: document.getElementById('username-display'), | |
| userInitials: document.getElementById('user-initials'), | |
| menuUsername: document.getElementById('menu-username'), | |
| userMenuBtn: document.getElementById('user-menu-btn'), | |
| userMenu: document.getElementById('user-menu'), | |
| logoutBtn: document.getElementById('logout-btn'), | |
| menuLogoutBtn: document.getElementById('menu-logout-btn'), | |
| // Dashboard elements | |
| totalProducts: document.getElementById('total-products'), | |
| availableProducts: document.getElementById('available-products'), | |
| lowStockProducts: document.getElementById('low-stock-products'), | |
| outOfStockProducts: document.getElementById('out-of-stock-products'), | |
| categoriesChart: document.getElementById('categories-chart'), | |
| recentProducts: document.getElementById('recent-products'), | |
| // Products list elements | |
| productSearch: document.getElementById('product-search'), | |
| refreshProducts: document.getElementById('refresh-products'), | |
| productsTable: document.getElementById('products-table'), | |
| showingFrom: document.getElementById('showing-from'), | |
| showingTo: document.getElementById('showing-to'), | |
| totalItems: document.getElementById('total-items'), | |
| prevPage: document.getElementById('prev-page'), | |
| nextPage: document.getElementById('next-page'), | |
| // Add product elements | |
| addProductForm: document.getElementById('add-product-form'), | |
| // Edit product modal elements | |
| editProductModal: document.getElementById('edit-product-modal'), | |
| closeEditModal: document.getElementById('close-edit-modal'), | |
| editProductForm: document.getElementById('edit-product-form'), | |
| editProductId: document.getElementById('edit-product-id'), | |
| editProductName: document.getElementById('edit-product-name'), | |
| editProductCategory: document.getElementById('edit-product-category'), | |
| editProductPrice: document.getElementById('edit-product-price'), | |
| editProductQuantity: document.getElementById('edit-product-quantity'), | |
| editProductDescription: document.getElementById('edit-product-description'), | |
| cancelEdit: document.getElementById('cancel-edit'), | |
| // Alert notification | |
| alertNotification: document.getElementById('alert-notification'), | |
| alertTitle: document.getElementById('alert-title'), | |
| alertMessage: document.getElementById('alert-message'), | |
| closeAlert: document.getElementById('close-alert'), | |
| // Section contents | |
| sectionContents: document.querySelectorAll('.section-content'), | |
| navLinks: document.querySelectorAll('.nav-item') | |
| }; | |
| // Helper functions | |
| const helpers = { | |
| // Show alert notification | |
| showAlert: (title, message, type = 'success') => { | |
| elements.alertTitle.textContent = title; | |
| elements.alertMessage.textContent = message; | |
| // Update alert style based on type | |
| const alertContainer = elements.alertNotification.querySelector('div'); | |
| alertContainer.className = `flex items-center px-4 py-3 border-l-4 ${type === 'success' ? 'border-green-500' : 'border-red-500'}`; | |
| const icon = elements.alertNotification.querySelector('div > div:first-child'); | |
| icon.className = `${type === 'success' ? 'text-green-500' : 'text-red-500'} mr-3`; | |
| icon.innerHTML = type === 'success' ? '<i class="fas fa-check-circle text-xl"></i>' : '<i class="fas fa-exclamation-circle text-xl"></i>'; | |
| elements.alertNotification.classList.remove('hidden'); | |
| // Auto hide after 5 seconds | |
| setTimeout(() => { | |
| elements.alertNotification.classList.add('hidden'); | |
| }, 5000); | |
| }, | |
| // Get initials from name | |
| getInitials: (name) => { | |
| return name.split(' ').map(part => part[0]).join('').toUpperCase(); | |
| }, | |
| // Format currency | |
| formatCurrency: (amount) => { | |
| return new Intl.NumberFormat('es-ES', { style: 'currency', currency: 'USD' }).format(amount); | |
| }, | |
| // Generate basic auth header | |
| getAuthHeader: () => { | |
| if (!state.currentUser) return {}; | |
| // Basic Auth: 'Basic ' + btoa(username + ':' + password) | |
| const token = btoa(`${state.currentUser.username}:${state.currentUser.password}`); | |
| return { | |
| 'Authorization': `Basic ${token}`, | |
| 'Content-Type': 'application/json' | |
| }; | |
| }, | |
| // Handle API errors | |
| handleApiError: (error) => { | |
| console.error('API Error:', error); | |
| helpers.showAlert('Error', error.message || 'Ocurrió un error al procesar la solicitud', 'error'); | |
| } | |
| }; | |
| // API functions | |
| const api = { | |
| // Login user | |
| login: async (username, password) => { | |
| try { | |
| // In a real app, this would call your backend API | |
| // For demo purposes, we're using mock data | |
| // Mock users (in a real app, these would come from your backend) | |
| const mockUsers = [ | |
| { username: 'admin', password: 'admin123', role: 'admin', name: 'Administrador' }, | |
| { username: 'user', password: 'user123', role: 'usuario', name: 'Usuario Normal' } | |
| ]; | |
| // Find user | |
| const user = mockUsers.find(u => u.username === username && u.password === password); | |
| if (!user) { | |
| throw new Error('Usuario o contraseña incorrectos'); | |
| } | |
| // Simulate API delay | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| return user; | |
| } catch (error) { | |
| throw error; | |
| } | |
| }, | |
| // Get products from API | |
| getProducts: async () => { | |
| try { | |
| // In a real app, this would call your backend API | |
| // For demo purposes, we're using mock data | |
| // Mock products | |
| const mockProducts = [ | |
| { id: 1, nombre: 'Laptop HP', categoria: 'Electrónicos', precio: 1200.50, cantidad: 15, descripcion: 'Laptop HP con 16GB RAM y 512GB SSD' }, | |
| { id: 2, nombre: 'Smartphone Samsung', categoria: 'Electrónicos', precio: 899.99, cantidad: 25, descripcion: 'Smartphone Samsung Galaxy S21' }, | |
| { id: 3, nombre: 'Camiseta Algodón', categoria: 'Ropa', precio: 24.99, cantidad: 50, descripcion: 'Camiseta 100% algodón talla M' }, | |
| { id: 4, nombre: 'Arroz Integral', categoria: 'Alimentos', precio: 3.50, cantidad: 100, descripcion: 'Arroz integral 1kg' }, | |
| { id: 5, nombre: 'Silla Oficina', categoria: 'Oficina', precio: 149.99, cantidad: 10, descripcion: 'Silla ergonómica para oficina' }, | |
| { id: 6, nombre: 'Lámpara LED', categoria: 'Hogar', precio: 45.75, cantidad: 30, descripcion: 'Lámpara LED de techo' }, | |
| { id: 7, nombre: 'Tablet Lenovo', categoria: 'Electrónicos', precio: 349.99, cantidad: 5, descripcion: 'Tablet Lenovo 10 pulgadas' }, | |
| { id: 8, nombre: 'Pantalón Vaquero', categoria: 'Ropa', precio: 39.99, cantidad: 20, descripcion: 'Pantalón vaquero talla 32' }, | |
| { id: 9, nombre: 'Aceite Oliva', categoria: 'Alimentos', precio: 8.99, cantidad: 40, descripcion: 'Aceite de oliva virgen extra 1L' }, | |
| { id: 10, nombre: 'Escritorio Madera', categoria: 'Oficina', precio: 199.99, cantidad: 8, descripcion: 'Escritorio de madera 120x60cm' } | |
| ]; | |
| // Simulate API delay | |
| await new Promise(resolve => setTimeout(resolve, 800)); | |
| return mockProducts; | |
| } catch (error) { | |
| throw error; | |
| } | |
| }, | |
| // Add new product | |
| addProduct: async (productData) => { | |
| try { | |
| // In a real app, this would POST to your backend API | |
| // Example using fetch: | |
| /* | |
| const response = await fetch(`${config.baseUrl}${config.endpoints.products}`, { | |
| method: 'POST', | |
| headers: helpers.getAuthHeader(), | |
| body: JSON.stringify(productData) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Error al agregar el producto'); | |
| } | |
| return await response.json(); | |
| */ | |
| // For demo purposes, we're just returning the product with a new ID | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| return { | |
| ...productData, | |
| id: Math.floor(Math.random() * 1000) + 11 // Generate a random ID | |
| }; | |
| } catch (error) { | |
| throw error; | |
| } | |
| }, | |
| // Update product | |
| updateProduct: async (productId, productData) => { | |
| try { | |
| // In a real app, this would PUT to your backend API | |
| // Example using fetch: | |
| /* | |
| const response = await fetch(`${config.baseUrl}${config.endpoints.products}/${productId}`, { | |
| method: 'PUT', | |
| headers: helpers.getAuthHeader(), | |
| body: JSON.stringify(productData) | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Error al actualizar el producto'); | |
| } | |
| return await response.json(); | |
| */ | |
| // For demo purposes, we're just returning the updated product | |
| await new Promise(resolve => setTimeout(resolve, 500)); | |
| return { | |
| ...productData, | |
| id: productId | |
| }; | |
| } catch (error) { | |
| throw error; | |
| } | |
| }, | |
| // Delete product | |
| deleteProduct: async (productId) => { | |
| try { | |
| // In a real app, this would DELETE to your backend API | |
| // Example using fetch: | |
| /* | |
| const response = await fetch(`${config.baseUrl}${config.endpoints.products}/${productId}`, { | |
| method: 'DELETE', | |
| headers: helpers.getAuthHeader() | |
| }); | |
| if (!response.ok) { | |
| throw new Error('Error al eliminar el producto'); | |
| } | |
| */ | |
| // For demo purposes, we're just simulating a successful deletion | |
| await new Promise(resolve => setTimeout(resolve, 300)); | |
| return true; | |
| } catch (error) { | |
| throw error; | |
| } | |
| } | |
| }; | |
| // UI functions | |
| const ui = { | |
| // Toggle sidebar | |
| toggleSidebar: () => { | |
| elements.sidebar.classList.toggle('collapsed'); | |
| elements.content.classList.toggle('expanded'); | |
| // Update localStorage | |
| const isCollapsed = elements.sidebar.classList.contains('collapsed'); | |
| localStorage.setItem('sidebarCollapsed', isCollapsed); | |
| }, | |
| // Switch between sections | |
| switchSection: (sectionId) => { | |
| // Update active nav link | |
| elements.navLinks.forEach(link => { | |
| link.classList.remove('bg-gray-700', 'text-white'); | |
| link.classList.add('text-gray-300'); | |
| if (link.getAttribute('data-section') === sectionId) { | |
| link.classList.remove('text-gray-300'); | |
| link.classList.add('bg-gray-700', 'text-white'); | |
| } | |
| }); | |
| // Hide all sections | |
| elements.sectionContents.forEach(section => { | |
| section.classList.add('hidden'); | |
| }); | |
| // Show selected section | |
| document.getElementById(sectionId).classList.remove('hidden'); | |
| state.currentSection = sectionId; | |
| // Update section title | |
| let title = 'Resumen'; | |
| if (sectionId === 'products-section') title = 'Productos'; | |
| if (sectionId === 'add-product-section') title = 'Agregar Producto'; | |
| elements.sectionTitle.textContent = title; | |
| // Load section data if needed | |
| if (sectionId === 'products-section') { | |
| ui.loadProductsTable(); | |
| } | |
| }, | |
| // Load dashboard summary | |
| loadDashboardSummary: () => { | |
| const totalProducts = state.products.length; | |
| const availableProducts = state.products.filter(p => p.cantidad > 10).length; | |
| const lowStockProducts = state.products.filter(p => p.cantidad > 0 && p.cantidad <= 10).length; | |
| const outOfStockProducts = state.products.filter(p => p.cantidad === 0).length; | |
| elements.totalProducts.textContent = totalProducts; | |
| elements.availableProducts.textContent = availableProducts; | |
| elements.lowStockProducts.textContent = lowStockProducts; | |
| elements.outOfStockProducts.textContent = outOfStockProducts; | |
| // Load categories chart | |
| const categories = {}; | |
| state.products.forEach(product => { | |
| if (!categories[product.categoria]) { | |
| categories[product.categoria] = 0; | |
| } | |
| categories[product.categoria]++; | |
| }); | |
| let categoriesHtml = ''; | |
| for (const [category, count] of Object.entries(categories)) { | |
| const colors = { | |
| 'Electrónicos': 'bg-blue-100 text-blue-800', | |
| 'Ropa': 'bg-purple-100 text-purple-800', | |
| 'Alimentos': 'bg-green-100 text-green-800', | |
| 'Oficina': 'bg-yellow-100 text-yellow-800', | |
| 'Hogar': 'bg-red-100 text-red-800' | |
| }; | |
| categoriesHtml += ` | |
| <div class="bg-white rounded-lg shadow p-4"> | |
| <div class="flex items-center"> | |
| <div class="p-2 rounded-full ${colors[category] || 'bg-gray-100 text-gray-800'}"> | |
| <i class="fas ${category === 'Electrónicos' ? 'fa-laptop' : | |
| category === 'Ropa' ? 'fa-tshirt' : | |
| category === 'Alimentos' ? 'fa-utensils' : | |
| category === 'Oficina' ? 'fa-briefcase' : 'fa-home'}"></i> | |
| </div> | |
| <div class="ml-3"> | |
| <p class="text-sm font-medium text-gray-500">${category}</p> | |
| <p class="text-lg font-semibold text-gray-800">${count} productos</p> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| elements.categoriesChart.innerHTML = categoriesHtml; | |
| // Load recent products (last 5 added) | |
| const recentProducts = [...state.products].sort((a, b) => b.id - a.id).slice(0, 5); | |
| let recentProductsHtml = ''; | |
| recentProducts.forEach(product => { | |
| recentProductsHtml += ` | |
| <tr> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${product.nombre}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${product.categoria}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${helpers.formatCurrency(product.precio)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${product.cantidad > 10 ? 'bg-green-100 text-green-800' : product.cantidad > 0 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800'}"> | |
| ${product.cantidad} ${product.cantidad === 1 ? 'unidad' : 'unidades'} | |
| </span> | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| elements.recentProducts.innerHTML = recentProductsHtml; | |
| }, | |
| // Load products table | |
| loadProductsTable: (products = state.filteredProducts.length ? state.filteredProducts : state.products) => { | |
| const startIndex = (config.pagination.currentPage - 1) * config.pagination.pageSize; | |
| const endIndex = startIndex + config.pagination.pageSize; | |
| const paginatedProducts = products.slice(startIndex, endIndex); | |
| let productsHtml = ''; | |
| paginatedProducts.forEach(product => { | |
| productsHtml += ` | |
| <tr> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <div class="flex-shrink-0 h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-600"> | |
| <i class="fas ${product.categoria === 'Electrónicos' ? 'fa-laptop' : | |
| product.categoria === 'Ropa' ? 'fa-tshirt' : | |
| product.categoria === 'Alimentos' ? 'fa-utensils' : | |
| product.categoria === 'Oficina' ? 'fa-briefcase' : 'fa-home'}"></i> | |
| </div> | |
| <div class="ml-4"> | |
| <div class="text-sm font-medium text-gray-900">${product.nombre}</div> | |
| <div class="text-sm text-gray-500">${product.descripcion.substring(0, 30)}${product.descripcion.length > 30 ? '...' : ''}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${product.categoria}</td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${helpers.formatCurrency(product.precio)}</td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${product.cantidad > 10 ? 'bg-green-100 text-green-800' : product.cantidad > 0 ? 'bg-yellow-100 text-yellow-800' : 'bg-red-100 text-red-800'}"> | |
| ${product.cantidad} ${product.cantidad === 1 ? 'unidad' : 'unidades'} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | |
| <button class="text-blue-600 hover:text-blue-900 mr-3 edit-product" data-id="${product.id}"> | |
| <i class="fas fa-edit"></i> | |
| </button> | |
| ${state.isAdmin ? ` | |
| <button class="text-red-600 hover:text-red-900 delete-product" data-id="${product.id}"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| ` : ''} | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| elements.productsTable.innerHTML = productsHtml; | |
| // Update pagination info | |
| elements.showingFrom.textContent = startIndex + 1; | |
| elements.showingTo.textContent = Math.min(endIndex, products.length); | |
| elements.totalItems.textContent = products.length; | |
| // Enable/disable pagination buttons | |
| elements.prevPage.disabled = config.pagination.currentPage === 1; | |
| elements.nextPage.disabled = endIndex >= products.length; | |
| // Add event listeners to edit and delete buttons | |
| document.querySelectorAll('.edit-product').forEach(btn => { | |
| btn.addEventListener('click', () => ui.showEditProductModal(btn.getAttribute('data-id'))); | |
| }); | |
| if (state.isAdmin) { | |
| document.querySelectorAll('.delete-product').forEach(btn => { | |
| btn.addEventListener('click', () => ui.deleteProduct(btn.getAttribute('data-id'))); | |
| }); | |
| } | |
| }, | |
| // Show edit product modal | |
| showEditProductModal: (productId) => { | |
| const product = state.products.find(p => p.id == productId); | |
| if (!product) return; | |
| elements.editProductId.value = product.id; | |
| elements.editProductName.value = product.nombre; | |
| elements.editProductCategory.value = product.categoria; | |
| elements.editProductPrice.value = product.precio; | |
| elements.editProductQuantity.value = product.cantidad; | |
| elements.editProductDescription.value = product.descripcion || ''; | |
| elements.editProductModal.classList.remove('hidden'); | |
| }, | |
| // Hide edit product modal | |
| hideEditProductModal: () => { | |
| elements.editProductModal.classList.add('hidden'); | |
| }, | |
| // Delete product | |
| deleteProduct: async (productId) => { | |
| if (!confirm('¿Estás seguro de que deseas eliminar este producto?')) return; | |
| try { | |
| await api.deleteProduct(productId); | |
| // Remove product from state | |
| state.products = state.products.filter(p => p.id != productId); | |
| state.filteredProducts = state.filteredProducts.filter(p => p.id != productId); | |
| // Reload products table | |
| ui.loadProductsTable(); | |
| // Update dashboard summary | |
| if (state.currentSection === 'dashboard-section') { | |
| ui.loadDashboardSummary(); | |
| } | |
| helpers.showAlert('Éxito', 'Producto eliminado correctamente'); | |
| } catch (error) { | |
| helpers.handleApiError(error); | |
| } | |
| }, | |
| // Search products | |
| searchProducts: (query) => { | |
| if (!query) { | |
| state.filteredProducts = []; | |
| ui.loadProductsTable(); | |
| return; | |
| } | |
| const lowerQuery = query.toLowerCase(); | |
| state.filteredProducts = state.products.filter(product => | |
| product.nombre.toLowerCase().includes(lowerQuery) || | |
| product.categoria.toLowerCase().includes(lowerQuery) || | |
| product.descripcion?.toLowerCase().includes(lowerQuery) | |
| ); | |
| // Reset to first page | |
| config.pagination.currentPage = 1; | |
| ui.loadProductsTable(); | |
| } | |
| }; | |
| // Event listeners | |
| const setupEventListeners = () => { | |
| // Login form | |
| elements.loginForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const username = elements.username.value; | |
| const password = elements.password.value; | |
| try { | |
| const user = await api.login(username, password); | |
| // Update application state | |
| state.currentUser = user; | |
| state.isAdmin = user.role === 'admin'; | |
| // Hide login screen and show dashboard | |
| elements.loginScreen.classList.add('hidden'); | |
| elements.dashboard.classList.remove('hidden'); | |
| // Update UI with user info | |
| elements.usernameDisplay.textContent = user.name; | |
| elements.menuUsername.textContent = user.username; | |
| elements.userInitials.textContent = helpers.getInitials(user.name); | |
| // Load initial data | |
| state.products = await api.getProducts(); | |
| // Load dashboard | |
| ui.loadDashboardSummary(); | |
| // Check sidebar state from localStorage | |
| if (localStorage.getItem('sidebarCollapsed') === 'true') { | |
| elements.sidebar.classList.add('collapsed'); | |
| elements.content.classList.add('expanded'); | |
| } | |
| // Show welcome message | |
| helpers.showAlert('Bienvenido', `Has iniciado sesión como ${user.name}`); | |
| } catch (error) { | |
| elements.loginError.classList.remove('hidden'); | |
| elements.loginError.textContent = error.message; | |
| } | |
| }); | |
| // Logout buttons | |
| elements.logoutBtn.addEventListener('click', () => { | |
| state.currentUser = null; | |
| elements.dashboard.classList.add('hidden'); | |
| elements.loginScreen.classList.remove('hidden'); | |
| elements.loginError.classList.add('hidden'); | |
| elements.loginForm.reset(); | |
| }); | |
| elements.menuLogoutBtn.addEventListener('click', () => { | |
| state.currentUser = null; | |
| elements.dashboard.classList.add('hidden'); | |
| elements.loginScreen.classList.remove('hidden'); | |
| elements.loginError.classList.add('hidden'); | |
| elements.loginForm.reset(); | |
| }); | |
| // Toggle sidebar | |
| elements.toggleSidebar.addEventListener('click', ui.toggleSidebar); | |
| // User menu | |
| elements.userMenuBtn.addEventListener('click', () => { | |
| elements.userMenu.classList.toggle('hidden'); | |
| }); | |
| // Close user menu when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!elements.userMenuBtn.contains(e.target) && !elements.userMenu.contains(e.target)) { | |
| elements.userMenu.classList.add('hidden'); | |
| } | |
| }); | |
| // Navigation links | |
| elements.navLinks.forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| ui.switchSection(link.getAttribute('data-section')); | |
| }); | |
| }); | |
| // Product search | |
| elements.productSearch.addEventListener('input', (e) => { | |
| ui.searchProducts(e.target.value); | |
| }); | |
| // Refresh products | |
| elements.refreshProducts.addEventListener('click', async () => { | |
| try { | |
| state.products = await api.getProducts(); | |
| state.filteredProducts = []; | |
| elements.productSearch.value = ''; | |
| ui.loadProductsTable(); | |
| helpers.showAlert('Éxito', 'Productos actualizados correctamente'); | |
| } catch (error) { | |
| helpers.handleApiError(error); | |
| } | |
| }); | |
| // Pagination buttons | |
| elements.prevPage.addEventListener('click', () => { | |
| if (config.pagination.currentPage > 1) { | |
| config.pagination.currentPage--; | |
| ui.loadProductsTable(); | |
| } | |
| }); | |
| elements.nextPage.addEventListener('click', () => { | |
| const totalPages = Math.ceil((state.filteredProducts.length || state.products.length) / config.pagination.pageSize); | |
| if (config.pagination.currentPage < totalPages) { | |
| config.pagination.currentPage++; | |
| ui.loadProductsTable(); | |
| } | |
| }); | |
| // Add product form | |
| elements.addProductForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const productData = { | |
| nombre: elements.productName.value, | |
| categoria: elements.productCategory.value, | |
| precio: parseFloat(elements.productPrice.value), | |
| cantidad: parseInt(elements.productQuantity.value), | |
| descripcion: elements.productDescription.value | |
| }; | |
| try { | |
| const newProduct = await api.addProduct(productData); | |
| // Add product to state | |
| state.products.unshift(newProduct); | |
| // Reset form | |
| elements.addProductForm.reset(); | |
| // Reload dashboard or products table | |
| if (state.currentSection === 'dashboard-section') { | |
| ui.loadDashboardSummary(); | |
| } else { | |
| ui.loadProductsTable(); | |
| } | |
| helpers.showAlert('Éxito', 'Producto agregado correctamente'); | |
| } catch (error) { | |
| helpers.handleApiError(error); | |
| } | |
| }); | |
| // Edit product form | |
| elements.editProductForm.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| const productId = elements.editProductId.value; | |
| const productData = { | |
| nombre: elements.editProductName.value, | |
| categoria: elements.editProductCategory.value, | |
| precio: parseFloat(elements.editProductPrice.value), | |
| cantidad: parseInt(elements.editProductQuantity.value), | |
| descripcion: elements.editProductDescription.value | |
| }; | |
| try { | |
| const updatedProduct = await api.updateProduct(productId, productData); | |
| // Update product in state | |
| const index = state.products.findIndex(p => p.id == productId); | |
| if (index !== -1) { | |
| state.products[index] = updatedProduct; | |
| } | |
| // Update filtered products if needed | |
| const filteredIndex = state.filteredProducts.findIndex(p => p.id == productId); | |
| if (filteredIndex !== -1) { | |
| state.filteredProducts[filteredIndex] = updatedProduct; | |
| } | |
| // Hide modal | |
| ui.hideEditProductModal(); | |
| // Reload current view | |
| if (state.currentSection === 'dashboard-section') { | |
| ui.loadDashboardSummary(); | |
| } else { | |
| ui.loadProductsTable(); | |
| } | |
| helpers.showAlert('Éxito', 'Producto actualizado correctamente'); | |
| } catch (error) { | |
| helpers.handleApiError(error); | |
| } | |
| }); | |
| // Close edit modal buttons | |
| elements.closeEditModal.addEventListener('click', ui.hideEditProductModal); | |
| elements.cancelEdit.addEventListener('click', ui.hideEditProductModal); | |
| // Close alert | |
| elements.closeAlert.addEventListener('click', () => { | |
| elements.alertNotification.classList.add('hidden'); | |
| }); | |
| }; | |
| // Initialize the application | |
| const init = () => { | |
| setupEventListeners(); | |
| // For demo purposes, pre-fill login form | |
| elements.username.value = 'admin'; | |
| elements.password.value = 'admin123'; | |
| }; | |
| // Start the app when DOM is loaded | |
| document.addEventListener('DOMContentLoaded', init); | |
| </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-v2" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |