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);
|
||||
}
|
||||
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 = `
|
||||
<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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
105
index.html
105
index.html
@@ -417,22 +417,86 @@
|
||||
<!-- Pestaña de Productos -->
|
||||
<div id="tab-products" class="tab-content">
|
||||
<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">
|
||||
<h3>Añadir/Editar</h3>
|
||||
<h3>Añadir/Editar Producto</h3>
|
||||
<form id="formProduct">
|
||||
<input type="hidden" id="p-id" />
|
||||
<div class="form-grid">
|
||||
<label>Nombre:</label>
|
||||
<input type="text" id="p-name" required />
|
||||
<label>Tipo:</label>
|
||||
<label>Descripción:</label>
|
||||
<input type="text" id="p-name" required placeholder="Nombre del producto/servicio" />
|
||||
<label>Tipo/Categoría:</label>
|
||||
<select id="p-type" required>
|
||||
<option value="service">Servicio</option>
|
||||
<option value="course">Curso</option>
|
||||
<option value="anticipo">Anticipo</option>
|
||||
</select>
|
||||
<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>
|
||||
|
||||
<!-- 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">
|
||||
<button type="submit">Guardar</button>
|
||||
<button type="reset" id="btnCancelEditProduct" class="btn-danger">Cancelar</button>
|
||||
@@ -442,31 +506,18 @@
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- Tabla unificada -->
|
||||
<div class="sub-section">
|
||||
<h3>Servicios</h3>
|
||||
<h3>Todos los Productos</h3>
|
||||
<div class="table-wrapper">
|
||||
<table id="tblServices">
|
||||
<table id="tblAllProducts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Precio</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<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 onclick="sortTable('folio')">Folio <span class="sort-icon">↕</span></th>
|
||||
<th onclick="sortTable('fecha')">Fecha <span class="sort-icon">↕</span></th>
|
||||
<th onclick="sortTable('cita')">Cita <span class="sort-icon">↕</span></th>
|
||||
<th onclick="sortTable('descripcion')">Descripción <span class="sort-icon">↕</span></th>
|
||||
<th onclick="sortTable('categoria')">Categoría <span class="sort-icon">↕</span></th>
|
||||
<th>Precio</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
|
||||
133
styles.css
133
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user