mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
feat: Implement unified products table with anticipos management
- 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 <noreply@anthropic.com>
This commit is contained in:
314
app.js
314
app.js
@@ -1050,6 +1050,7 @@ async function handleAddOrUpdateProduct(e) {
|
|||||||
products.push(result);
|
products.push(result);
|
||||||
}
|
}
|
||||||
renderProductTables();
|
renderProductTables();
|
||||||
|
updateUnifiedProductsAfterChange(); // Actualizar tabla unificada
|
||||||
formProduct.reset();
|
formProduct.reset();
|
||||||
document.getElementById('p-id').value = '';
|
document.getElementById('p-id').value = '';
|
||||||
} else {
|
} else {
|
||||||
@@ -1067,6 +1068,7 @@ async function deleteProduct(id) {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
products = products.filter(p => p.id !== id);
|
products = products.filter(p => p.id !== id);
|
||||||
renderProductTables();
|
renderProductTables();
|
||||||
|
updateUnifiedProductsAfterChange(); // Actualizar tabla unificada
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('Error de conexión al eliminar el producto.');
|
alert('Error de conexión al eliminar el producto.');
|
||||||
@@ -1763,6 +1765,8 @@ async function initializeApp() {
|
|||||||
populateFooter();
|
populateFooter();
|
||||||
console.log('Initializing dynamic system...');
|
console.log('Initializing dynamic system...');
|
||||||
initializeDynamicSystem();
|
initializeDynamicSystem();
|
||||||
|
console.log('Initializing unified products table...');
|
||||||
|
initializeUnifiedTable();
|
||||||
console.log('Initialization complete.');
|
console.log('Initialization complete.');
|
||||||
|
|
||||||
}).catch(error => {
|
}).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 = `
|
||||||
|
<td>${item.folio}</td>
|
||||||
|
<td>${item.fecha}</td>
|
||||||
|
<td>${item.cita || 'N/A'}</td>
|
||||||
|
<td>${escapeHTML(item.descripcion)}</td>
|
||||||
|
<td><span class="category-badge ${categoryClass}">${getCategoryName(item.categoria)}</span></td>
|
||||||
|
<td>$${parseFloat(item.precio || 0).toFixed(2)}</td>
|
||||||
|
<td>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button class="btn-icon btn-edit" onclick="editUnifiedProduct('${item.id}')" title="Editar">
|
||||||
|
<span class="material-icons-outlined">edit</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon btn-cancel" onclick="toggleProductStatus('${item.id}')" title="${item.status === 'cancelled' ? 'Reactivar' : 'Cancelar'}">
|
||||||
|
<span class="material-icons-outlined">${item.status === 'cancelled' ? 'check_circle' : 'cancel'}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn-icon btn-delete" onclick="deleteUnifiedProduct('${item.id}')" title="Eliminar">
|
||||||
|
<span class="material-icons-outlined">delete</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
ap-pos:
|
ap-pos:
|
||||||
image: coderk/ap_pos:1.3.5
|
image: marcogll/ap_pos:1.3.5
|
||||||
container_name: ap-pos
|
container_name: ap-pos
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
105
index.html
105
index.html
@@ -417,22 +417,86 @@
|
|||||||
<!-- Pestaña de Productos -->
|
<!-- Pestaña de Productos -->
|
||||||
<div id="tab-products" class="tab-content">
|
<div id="tab-products" class="tab-content">
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Gestión de Productos y Cursos</h2>
|
<h2>Gestión de Productos, Servicios y Anticipos</h2>
|
||||||
|
|
||||||
|
<!-- Controles de filtrado y ordenamiento -->
|
||||||
|
<div class="filter-controls">
|
||||||
|
<div class="filter-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Filtrar por tipo:</label>
|
||||||
|
<select id="filter-type">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="service">Servicios</option>
|
||||||
|
<option value="course">Cursos</option>
|
||||||
|
<option value="anticipo">Anticipos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ordenar por:</label>
|
||||||
|
<select id="sort-by">
|
||||||
|
<option value="descripcion">Descripción</option>
|
||||||
|
<option value="categoria">Categoría</option>
|
||||||
|
<option value="fecha">Fecha</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Búsqueda:</label>
|
||||||
|
<input type="text" id="search-products" placeholder="Buscar por descripción..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario para añadir/editar -->
|
||||||
<div class="sub-section">
|
<div class="sub-section">
|
||||||
<h3>Añadir/Editar</h3>
|
<h3>Añadir/Editar Producto</h3>
|
||||||
<form id="formProduct">
|
<form id="formProduct">
|
||||||
<input type="hidden" id="p-id" />
|
<input type="hidden" id="p-id" />
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<label>Nombre:</label>
|
<label>Descripción:</label>
|
||||||
<input type="text" id="p-name" required />
|
<input type="text" id="p-name" required placeholder="Nombre del producto/servicio" />
|
||||||
<label>Tipo:</label>
|
<label>Tipo/Categoría:</label>
|
||||||
<select id="p-type" required>
|
<select id="p-type" required>
|
||||||
<option value="service">Servicio</option>
|
<option value="service">Servicio</option>
|
||||||
<option value="course">Curso</option>
|
<option value="course">Curso</option>
|
||||||
|
<option value="anticipo">Anticipo</option>
|
||||||
</select>
|
</select>
|
||||||
<label>Precio (MXN):</label>
|
<label>Precio (MXN):</label>
|
||||||
<input type="number" id="p-price" step="0.01" min="0" />
|
<input type="number" id="p-price" step="0.01" min="0" placeholder="0.00" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Campos adicionales para anticipos -->
|
||||||
|
<div id="anticipo-fields" class="form-grid" style="display: none;">
|
||||||
|
<label>Fecha de cita:</label>
|
||||||
|
<div class="date-time-container">
|
||||||
|
<input type="number" id="p-cita-dia" min="1" max="31" placeholder="DD" class="date-field" />
|
||||||
|
<span class="date-separator">/</span>
|
||||||
|
<input type="number" id="p-cita-mes" min="1" max="12" placeholder="MM" class="date-field" />
|
||||||
|
<span class="date-separator">/</span>
|
||||||
|
<input type="number" id="p-cita-año" min="2024" max="2030" placeholder="AAAA" class="date-field-year" />
|
||||||
|
</div>
|
||||||
|
<label>Hora de cita:</label>
|
||||||
|
<select id="p-hora-cita" class="time-select">
|
||||||
|
<option value="">-- Seleccionar hora --</option>
|
||||||
|
<option value="10:00">10:00 AM</option>
|
||||||
|
<option value="10:30">10:30 AM</option>
|
||||||
|
<option value="11:00">11:00 AM</option>
|
||||||
|
<option value="11:30">11:30 AM</option>
|
||||||
|
<option value="12:00">12:00 PM</option>
|
||||||
|
<option value="12:30">12:30 PM</option>
|
||||||
|
<option value="13:00">1:00 PM</option>
|
||||||
|
<option value="13:30">1:30 PM</option>
|
||||||
|
<option value="14:00">2:00 PM</option>
|
||||||
|
<option value="14:30">2:30 PM</option>
|
||||||
|
<option value="15:00">3:00 PM</option>
|
||||||
|
<option value="15:30">3:30 PM</option>
|
||||||
|
<option value="16:00">4:00 PM</option>
|
||||||
|
<option value="16:30">4:30 PM</option>
|
||||||
|
<option value="17:00">5:00 PM</option>
|
||||||
|
<option value="17:30">5:30 PM</option>
|
||||||
|
<option value="18:00">6:00 PM</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit">Guardar</button>
|
<button type="submit">Guardar</button>
|
||||||
<button type="reset" id="btnCancelEditProduct" class="btn-danger">Cancelar</button>
|
<button type="reset" id="btnCancelEditProduct" class="btn-danger">Cancelar</button>
|
||||||
@@ -442,31 +506,18 @@
|
|||||||
|
|
||||||
<hr class="section-divider">
|
<hr class="section-divider">
|
||||||
|
|
||||||
|
<!-- Tabla unificada -->
|
||||||
<div class="sub-section">
|
<div class="sub-section">
|
||||||
<h3>Servicios</h3>
|
<h3>Todos los Productos</h3>
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table id="tblServices">
|
<table id="tblAllProducts">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nombre</th>
|
<th onclick="sortTable('folio')">Folio <span class="sort-icon">↕</span></th>
|
||||||
<th>Precio</th>
|
<th onclick="sortTable('fecha')">Fecha <span class="sort-icon">↕</span></th>
|
||||||
<th>Acciones</th>
|
<th onclick="sortTable('cita')">Cita <span class="sort-icon">↕</span></th>
|
||||||
</tr>
|
<th onclick="sortTable('descripcion')">Descripción <span class="sort-icon">↕</span></th>
|
||||||
</thead>
|
<th onclick="sortTable('categoria')">Categoría <span class="sort-icon">↕</span></th>
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="section-divider">
|
|
||||||
|
|
||||||
<div class="sub-section">
|
|
||||||
<h3>Cursos</h3>
|
|
||||||
<div class="table-wrapper">
|
|
||||||
<table id="tblCourses">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Nombre</th>
|
|
||||||
<th>Precio</th>
|
<th>Precio</th>
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
133
styles.css
133
styles.css
@@ -47,6 +47,139 @@ h3 {
|
|||||||
font-size: 18px;
|
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 {
|
.section {
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user