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> |