From 43eca8269e17e8e7e08ff96fd06ee3e1d8dfd5f8 Mon Sep 17 00:00:00 2001 From: Marco Gallegos Date: Tue, 2 Sep 2025 15:07:32 -0600 Subject: [PATCH] feat: Implement unified products table with anticipos management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added unified table for services, courses, and anticipos in Products tab - Implemented table sorting by folio, date, appointment, and description - Added filtering by product type and real-time search functionality - Created action buttons with icons for edit, cancel/reactivate, and delete - Added special handling for anticipos with appointment date/time fields - Fixed JavaScript conflicts and function naming issues - Integrated with existing product management system - Added responsive design with category badges and status indicators 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- app.js | 314 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- index.html | 105 +++++++++++---- styles.css | 133 +++++++++++++++++++ 4 files changed, 526 insertions(+), 28 deletions(-) diff --git a/app.js b/app.js index 5f60890..e7494ce 100644 --- a/app.js +++ b/app.js @@ -1050,6 +1050,7 @@ async function handleAddOrUpdateProduct(e) { products.push(result); } renderProductTables(); + updateUnifiedProductsAfterChange(); // Actualizar tabla unificada formProduct.reset(); document.getElementById('p-id').value = ''; } else { @@ -1067,6 +1068,7 @@ async function deleteProduct(id) { if (response.ok) { products = products.filter(p => p.id !== id); renderProductTables(); + updateUnifiedProductsAfterChange(); // Actualizar tabla unificada } } catch (error) { alert('Error de conexión al eliminar el producto.'); @@ -1763,6 +1765,8 @@ async function initializeApp() { populateFooter(); console.log('Initializing dynamic system...'); initializeDynamicSystem(); + console.log('Initializing unified products table...'); + initializeUnifiedTable(); console.log('Initialization complete.'); }).catch(error => { @@ -1771,4 +1775,314 @@ async function initializeApp() { }); } +// --- NUEVA IMPLEMENTACIÓN: TABLA UNIFICADA DE PRODUCTOS --- + +// Estado global para la tabla unificada +let allProductsData = []; +let currentSortField = 'descripcion'; +let currentSortDirection = 'asc'; + +// Generar folio único para productos +function generateProductFolio(type) { + const folioPrefix = settings.folioPrefix || 'PRD'; + const timestamp = Date.now().toString().slice(-6); + const typeCode = { + 'service': 'SRV', + 'course': 'CRS', + 'anticipo': 'ANT' + }; + return `${folioPrefix}-${typeCode[type]}-${timestamp}`; +} + +// Construir fecha de cita para anticipos +function construirFechaCitaProducto() { + const dia = document.getElementById('p-cita-dia').value; + const mes = document.getElementById('p-cita-mes').value; + const año = document.getElementById('p-cita-año').value; + const hora = document.getElementById('p-hora-cita').value; + + if (!dia || !mes || !año) { + return ''; + } + + const diaStr = dia.padStart(2, '0'); + const mesStr = mes.padStart(2, '0'); + const horaStr = hora || '00:00'; + + return `${diaStr}/${mesStr}/${año} ${horaStr}`; +} + +// Renderizar tabla unificada +function renderUnifiedProductsTable() { + const tableBody = document.querySelector('#tblAllProducts tbody'); + if (!tableBody) return; + + // Limpiar tabla + tableBody.innerHTML = ''; + + // Aplicar filtros + let filteredData = [...allProductsData]; + + const filterType = document.getElementById('filter-type')?.value; + if (filterType) { + filteredData = filteredData.filter(item => item.categoria === filterType); + } + + const searchTerm = document.getElementById('search-products')?.value?.toLowerCase(); + if (searchTerm) { + filteredData = filteredData.filter(item => + item.descripcion.toLowerCase().includes(searchTerm) || + item.categoria.toLowerCase().includes(searchTerm) + ); + } + + // Ordenar datos + filteredData.sort((a, b) => { + let aValue = a[currentSortField] || ''; + let bValue = b[currentSortField] || ''; + + if (typeof aValue === 'string') { + aValue = aValue.toLowerCase(); + bValue = bValue.toLowerCase(); + } + + if (currentSortDirection === 'asc') { + return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; + } else { + return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; + } + }); + + // Renderizar filas + filteredData.forEach(item => { + const row = document.createElement('tr'); + + const statusClass = item.status === 'cancelled' ? 'status-cancelled' : 'status-active'; + const categoryClass = `category-${item.categoria}`; + + row.innerHTML = ` + ${item.folio} + ${item.fecha} + ${item.cita || 'N/A'} + ${escapeHTML(item.descripcion)} + ${getCategoryName(item.categoria)} + $${parseFloat(item.precio || 0).toFixed(2)} + +
+ + + +
+ + `; + + tableBody.appendChild(row); + }); +} + +// Obtener nombre amigable de categoría +function getCategoryName(categoria) { + const names = { + 'service': 'Servicio', + 'course': 'Curso', + 'anticipo': 'Anticipo' + }; + return names[categoria] || categoria; +} + +// Cargar datos unificados +function loadUnifiedProductsData() { + allProductsData = []; + + // Agregar productos existentes (servicios y cursos) + products.forEach(product => { + allProductsData.push({ + id: product.id, + folio: product.folio || generateProductFolio(product.type), + fecha: product.created_at ? new Date(product.created_at).toLocaleDateString('es-ES') : new Date().toLocaleDateString('es-ES'), + cita: '', // Los servicios y cursos no tienen cita predefinida + descripcion: product.name, + categoria: product.type, + precio: product.price || 0, + status: product.status || 'active' + }); + }); + + // Agregar anticipos desde movimientos + const anticiposMovements = movements.filter(m => + m.concepto && (m.concepto.includes('Anticipo') || m.concepto.includes('anticipo')) + ); + + anticiposMovements.forEach(anticipo => { + allProductsData.push({ + id: `anticipo-${anticipo.id}`, + folio: anticipo.folio, + fecha: new Date(anticipo.fecha).toLocaleDateString('es-ES'), + cita: anticipo.fechaCita ? new Date(anticipo.fechaCita).toLocaleDateString('es-ES') + ' ' + (anticipo.horaCita || '') : 'N/A', + descripcion: anticipo.concepto, + categoria: 'anticipo', + precio: anticipo.monto || 0, + status: anticipo.aplicado ? 'cancelled' : 'active' + }); + }); +} + +// Ordenar tabla +function sortTable(field) { + if (currentSortField === field) { + currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; + } else { + currentSortField = field; + currentSortDirection = 'asc'; + } + + // Actualizar iconos de ordenamiento + document.querySelectorAll('.sort-icon').forEach(icon => { + icon.textContent = '↕'; + }); + + const currentIcon = document.querySelector(`th[onclick="sortTable('${field}')"] .sort-icon`); + if (currentIcon) { + currentIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓'; + } + + renderUnifiedProductsTable(); +} + +// Editar producto unificado +function editUnifiedProduct(id) { + if (id.startsWith('anticipo-')) { + // Manejar edición de anticipo + const anticipoId = id.replace('anticipo-', ''); + const anticipo = movements.find(m => m.id == anticipoId); + if (anticipo) { + alert('La edición de anticipos se realiza desde el historial de ventas.'); + } + return; + } + + // Manejar edición de producto regular + const product = products.find(p => p.id == id); + if (product) { + document.getElementById('p-id').value = product.id; + document.getElementById('p-name').value = product.name; + document.getElementById('p-type').value = product.type; + document.getElementById('p-price').value = product.price || ''; + + // Mostrar/ocultar campos de anticipo + toggleAnticipoFields(product.type); + } +} + +// Cambiar estado del producto +async function toggleProductStatus(id) { + if (id.startsWith('anticipo-')) { + alert('El estado de los anticipos se maneja desde el sistema de ventas.'); + return; + } + + const product = products.find(p => p.id == id); + if (!product) return; + + const newStatus = product.status === 'cancelled' ? 'active' : 'cancelled'; + const actionText = newStatus === 'cancelled' ? 'cancelar' : 'reactivar'; + + if (confirm(`¿Estás seguro de que quieres ${actionText} este producto?`)) { + try { + const response = await fetch(`/api/products/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...product, + status: newStatus + }) + }); + + if (response.ok) { + product.status = newStatus; + loadUnifiedProductsData(); + renderUnifiedProductsTable(); + } + } catch (error) { + alert('Error de conexión al actualizar el producto.'); + } + } +} + +// Eliminar producto unificado +async function deleteUnifiedProduct(id) { + if (id.startsWith('anticipo-')) { + alert('Los anticipos no se pueden eliminar desde aquí. Usa el historial de ventas.'); + return; + } + + if (confirm('¿Estás seguro de que quieres eliminar este producto permanentemente?')) { + try { + const response = await fetch(`/api/products/${id}`, { method: 'DELETE' }); + if (response.ok) { + products = products.filter(p => p.id != id); + loadUnifiedProductsData(); + renderUnifiedProductsTable(); + renderProductTables(); // Actualizar también las tablas originales + } + } catch (error) { + alert('Error de conexión al eliminar el producto.'); + } + } +} + +// Mostrar/ocultar campos de anticipo +function toggleAnticipoFields(type) { + const anticipoFields = document.getElementById('anticipo-fields'); + if (anticipoFields) { + anticipoFields.style.display = type === 'anticipo' ? 'block' : 'none'; + } +} + +// Función para actualizar tabla unificada después de cambios +function updateUnifiedProductsAfterChange() { + loadUnifiedProductsData(); + renderUnifiedProductsTable(); +} + +// Inicializar controles de la tabla unificada +function initializeUnifiedTable() { + // Verificar que los elementos existan antes de agregar listeners + const filterType = document.getElementById('filter-type'); + const searchProducts = document.getElementById('search-products'); + const productTypeSelect = document.getElementById('p-type'); + + if (filterType) { + filterType.addEventListener('change', renderUnifiedProductsTable); + } + + if (searchProducts) { + searchProducts.addEventListener('input', renderUnifiedProductsTable); + } + + if (productTypeSelect) { + productTypeSelect.addEventListener('change', (e) => { + toggleAnticipoFields(e.target.value); + }); + } + + // Solo cargar si hay datos disponibles + if (typeof products !== 'undefined' && typeof movements !== 'undefined') { + loadUnifiedProductsData(); + renderUnifiedProductsTable(); + } +} + +// Exponer funciones globalmente para uso en onclick +window.sortTable = sortTable; +window.editUnifiedProduct = editUnifiedProduct; +window.toggleProductStatus = toggleProductStatus; +window.deleteUnifiedProduct = deleteUnifiedProduct; + document.addEventListener('DOMContentLoaded', initializeApp); diff --git a/docker-compose.yml b/docker-compose.yml index 4914a18..00d423c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: ap-pos: - image: coderk/ap_pos:1.3.5 + image: marcogll/ap_pos:1.3.5 container_name: ap-pos restart: unless-stopped ports: diff --git a/index.html b/index.html index d86455c..a9e77f6 100644 --- a/index.html +++ b/index.html @@ -417,22 +417,86 @@
-

Gestión de Productos y Cursos

+

Gestión de Productos, Servicios y Anticipos

+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
-

Añadir/Editar

+

Añadir/Editar Producto

- - - + + + - +
+ + + +
@@ -442,31 +506,18 @@
+
-

Servicios

+

Todos los Productos

- +
- - - - - - -
NombrePrecioAcciones
-
-
- -
- -
-

Cursos

-
- - - - + + + + + diff --git a/styles.css b/styles.css index d549d36..1d07b60 100644 --- a/styles.css +++ b/styles.css @@ -47,6 +47,139 @@ h3 { font-size: 18px; } +/* --- Controles de filtrado y búsqueda --- */ +.filter-controls { + margin-bottom: 20px; + padding: 15px; + background: #f8f9fa; + border-radius: 6px; + border: 1px solid #dee2e6; +} + +.filter-row { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + gap: 15px; + align-items: end; +} + +@media (max-width: 768px) { + .filter-row { + grid-template-columns: 1fr; + } +} + +/* --- Estilos para tabla unificada --- */ +.sort-icon { + font-size: 12px; + margin-left: 5px; + color: #6c757d; +} + +th[onclick] { + cursor: pointer; + user-select: none; +} + +th[onclick]:hover { + background-color: #f8f9fa; +} + +.action-buttons { + display: flex; + gap: 5px; + justify-content: center; +} + +.btn-icon { + background: none; + border: none; + padding: 5px; + cursor: pointer; + border-radius: 4px; + transition: background-color 0.2s; + font-size: 16px; +} + +.btn-icon:hover { + background-color: #f8f9fa; +} + +.btn-icon.btn-edit { + color: #007bff; +} + +.btn-icon.btn-cancel { + color: #ffc107; +} + +.btn-icon.btn-delete { + color: #dc3545; +} + +.btn-icon.btn-edit:hover { + background-color: #e7f3ff; +} + +.btn-icon.btn-cancel:hover { + background-color: #fff3cd; +} + +.btn-icon.btn-delete:hover { + background-color: #f8d7da; +} + +/* --- Campos adicionales para anticipos --- */ +#anticipo-fields { + margin-top: 15px; + padding: 15px; + background: #f8f9fa; + border-radius: 6px; + border: 1px solid #dee2e6; +} + +/* --- Estados de productos --- */ +.product-status { + padding: 3px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-align: center; +} + +.status-active { + background-color: #d4edda; + color: #155724; +} + +.status-cancelled { + background-color: #f8d7da; + color: #721c24; +} + +.category-badge { + padding: 3px 8px; + border-radius: 12px; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; +} + +.category-service { + background-color: #cce5ff; + color: #0066cc; +} + +.category-course { + background-color: #d1ecf1; + color: #0c5460; +} + +.category-anticipo { + background-color: #fff3cd; + color: #856404; +} + .section { margin-bottom: 40px; }
NombreFolio ↕Fecha ↕Cita ↕Descripción ↕Categoría ↕ Precio Acciones