test-v2 / index.html
fakesisalg's picture
Add 3 files
b0f61ce verified
<!DOCTYPE html>
<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>