mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
feat: Release version 1.5.0 with major UI improvements and advanced anticipos
- Add sales subtabs: Ventas and Tickets for better organization - Implement manual anticipos with confirmation checkbox system - Add automatic "Público General" for sales without specific client - Reorganize service order: Clean Girl → Elegant → Mystery → Seduction - Replace gradient with solid black header background - Improve price alignment with grid layout - Add sort_order field to products for consistent ordering - Move movements section from dashboard to Tickets subtab - Enhanced JavaScript functionality for subtab navigation - Remove TASKS.md documentation file 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
48
README.md
48
README.md
@@ -94,33 +94,31 @@ El sistema está diseñado para ser desplegado fácilmente utilizando Docker y D
|
||||
- Los datos se mantienen entre reinicios y actualizaciones del contenedor
|
||||
- Para más información sobre Docker, consulta [DOCKER.md](./DOCKER.md)
|
||||
|
||||
## Novedades de la Versión 1.3.5
|
||||
## Novedades de la Versión 1.5.0
|
||||
|
||||
### 🚀 **Nueva Interfaz de Ventas**
|
||||
- **Formulario modernizado**: Diseño más intuitivo y profesional
|
||||
- **Múltiples productos**: Agrega varios servicios/cursos en una sola venta
|
||||
- **Sistema de cantidades**: Especifica la cantidad de cada producto
|
||||
### 🎫 **Reorganización de Interface**
|
||||
- **Subpestañas en Ventas**: Nueva estructura con "💰 Ventas" y "🎫 Tickets"
|
||||
- **Dashboard limpio**: Movida sección de movimientos a subpestaña de Tickets
|
||||
- **Navegación mejorada**: Interfaz más organizada y lógica
|
||||
|
||||
### 💰 **Sistema de Descuentos Avanzado**
|
||||
- **Interfaz colapsable**: Sección de descuentos elegante y fácil de usar
|
||||
- **Dos tipos de descuento**: Por porcentaje (%) o monto fijo ($)
|
||||
- **Motivo del descuento**: Registro del motivo para auditoría
|
||||
- **Preview en tiempo real**: Ve el descuento aplicado instantáneamente
|
||||
### 💳 **Sistema de Anticipos Avanzado**
|
||||
- **Anticipos manuales**: Aplicar anticipos no registrados con confirmación
|
||||
- **Checkbox de seguridad**: Confirmación obligatoria para anticipos manuales
|
||||
- **Integración completa**: Anticipos se aplican como descuentos automáticamente
|
||||
- **Control de duplicación**: Sistema previene aplicar el mismo anticipo múltiples veces
|
||||
|
||||
### 📅 **Gestión de Citas Mejorada**
|
||||
- **Campos de fecha intuitivos**: DD/MM/AAAA más fácil de usar
|
||||
- **Horarios preconfigurados**: Selección rápida de horas disponibles
|
||||
- **Integración con ventas**: Cita programada directamente al crear la venta
|
||||
### 👥 **Gestión de Clientes Mejorada**
|
||||
- **Público General**: Sistema automático para ventas sin cliente específico
|
||||
- **Campo opcional**: Cliente ya no es obligatorio en ventas
|
||||
- **Tickets genéricos**: Soporte para ventas a público general
|
||||
|
||||
### 🧾 **Tickets Optimizados**
|
||||
- **Formato térmico 58mm**: Diseño específico para impresoras térmicas
|
||||
- **Información completa**: Productos, cantidades, descuentos y totales
|
||||
- **QR Code**: Para feedback de clientes
|
||||
- **Fechas corregidas**: Formato DD/MM/YYYY HH:MM sin errores de "undefined"
|
||||
- **Etiquetas en negrita**: Folio y Fecha destacados visualmente
|
||||
### 🎨 **Mejoras Visuales**
|
||||
- **Header sólido**: Eliminado gradiente por color sólido negro
|
||||
- **Precios alineados**: Grid layout mejorado para mejor presentación
|
||||
- **Orden de servicios**: Clean Girl → Elegant → Mystery → Seduction con sus retoques
|
||||
- **Interfaz consistente**: Colores y estilos uniformes
|
||||
|
||||
### ⚡ **Mejoras Técnicas**
|
||||
- **Cálculos en tiempo real**: Totales actualizados automáticamente
|
||||
- **Validaciones mejoradas**: Mejor control de errores
|
||||
- **Base de datos optimizada**: Persistencia de datos mejorada
|
||||
- **API REST**: Migración completa de localStorage a servidor
|
||||
### ⚡ **Optimizaciones Técnicas**
|
||||
- **Base de datos mejorada**: Campo sort_order para control de ordenamiento
|
||||
- **Subpestañas funcionales**: JavaScript para navegación entre secciones
|
||||
- **Validaciones reforzadas**: Mejor control de formularios y datos
|
||||
|
||||
442
index.html
442
index.html
@@ -12,7 +12,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@500;600&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Icons+Outlined" rel="stylesheet">
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="styles.css?v=1757039067" />
|
||||
<link rel="stylesheet" href="styles.css?v=1757039804" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -33,6 +33,9 @@
|
||||
<button type="button" class="tab-link" data-tab="tab-products">
|
||||
<span class="material-icons-outlined">inventory_2</span><span>Productos</span>
|
||||
</button>
|
||||
<button type="button" class="tab-link" data-tab="tab-cancellation-requests" id="tab-cancellation-requests-btn" style="display: none;">
|
||||
<span class="material-icons-outlined">cancel_presentation</span><span>Solicitudes</span>
|
||||
</button>
|
||||
<button type="button" class="tab-link" data-tab="tab-settings">
|
||||
<span class="material-icons-outlined">settings</span><span>Configuración</span>
|
||||
</button>
|
||||
@@ -77,30 +80,159 @@
|
||||
|
||||
<!-- Pestaña de Movimientos/Recibos -->
|
||||
<div id="tab-movements" class="tab-content">
|
||||
<div class="section">
|
||||
<h2>Nuevo Movimiento</h2>
|
||||
<form id="formMove" class="form-modern">
|
||||
<!-- Cliente -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Cliente</label>
|
||||
<input type="text" id="m-cliente" list="client-list" required autocomplete="off" placeholder="Nombre del cliente" />
|
||||
<datalist id="client-list"></datalist>
|
||||
<div class="sub-tabs">
|
||||
<button type="button" class="sub-tab-link active" data-subtab="sub-tab-ventas">💰 Ventas</button>
|
||||
<button type="button" class="sub-tab-link" data-subtab="sub-tab-tickets">🎫 Tickets</button>
|
||||
</div>
|
||||
|
||||
<!-- Sub-Pestaña de Ventas -->
|
||||
<div id="sub-tab-ventas" class="sub-tab-content active">
|
||||
<div class="sales-container">
|
||||
<!-- Header -->
|
||||
<div class="sales-header">
|
||||
<h2>💰 Nueva Venta</h2>
|
||||
<div class="sales-summary">
|
||||
<span class="cart-count" id="cart-count">0 productos</span>
|
||||
<span class="cart-total" id="cart-total">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sales-layout">
|
||||
<!-- Panel de Productos (Izquierda) -->
|
||||
<div class="products-panel">
|
||||
<!-- Cliente -->
|
||||
<div class="client-selector">
|
||||
<div class="form-group modern-input">
|
||||
<label>👤 Cliente</label>
|
||||
<input type="text" id="m-cliente" list="client-list" autocomplete="off" placeholder="Buscar o crear cliente... (opcional)" />
|
||||
<datalist id="client-list"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Categorías de Productos -->
|
||||
<div class="categories-container">
|
||||
<h3>Selecciona tus servicios</h3>
|
||||
|
||||
<!-- Vanity Lashes -->
|
||||
<div class="category-section" data-category="Vanity Lashes">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">👁️</span>
|
||||
<h4>Vanity Lashes</h4>
|
||||
<button class="category-toggle">▶</button>
|
||||
</div>
|
||||
<div class="products-grid" id="pestanas-products">
|
||||
<!-- Los productos se cargarán dinámicamente aquí -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PMU Services -->
|
||||
<div class="category-section" data-category="PMU Services">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">✏️</span>
|
||||
<h4>PMU Services</h4>
|
||||
<button class="category-toggle">▶</button>
|
||||
</div>
|
||||
<div class="products-grid" id="microblading-products">
|
||||
<!-- Los productos se cargarán dinámicamente aquí -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uñas -->
|
||||
<div class="category-section" data-category="Uñas">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">💅</span>
|
||||
<h4>Nail Art</h4>
|
||||
<button class="category-toggle">▶</button>
|
||||
</div>
|
||||
<div class="products-grid" id="unas-products">
|
||||
<!-- Los productos se cargarán dinámicamente aquí -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anticipos -->
|
||||
<div class="category-section" data-category="Anticipos">
|
||||
<div class="category-header">
|
||||
<span class="category-icon">💰</span>
|
||||
<h4>Anticipos</h4>
|
||||
<button class="category-toggle">▶</button>
|
||||
</div>
|
||||
<div class="anticipos-grid" id="anticipos-products">
|
||||
<div class="anticipo-form">
|
||||
<div class="anticipo-input-group">
|
||||
<input type="number" id="anticipo-amount" min="1" placeholder="Cantidad" />
|
||||
<input type="text" id="anticipo-comment" placeholder="Comentario (opcional)" />
|
||||
<button class="btn-add-anticipo" onclick="addAnticipo()">Agregar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cita -->
|
||||
<div class="form-section" id="appointment-section">
|
||||
<h3>Datos de la Cita</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Fecha de la Cita</label>
|
||||
<input type="date" id="m-fecha-cita" class="date-picker" />
|
||||
<!-- Panel de Carrito y Checkout (Derecha) -->
|
||||
<div class="checkout-panel">
|
||||
<!-- Carrito de Productos -->
|
||||
<div class="cart-section">
|
||||
<h3>🛒 Carrito de Compras</h3>
|
||||
<div id="selected-products-container" class="cart-items">
|
||||
<div class="empty-cart">
|
||||
<span class="empty-icon">🛒</span>
|
||||
<p>Selecciona servicios para comenzar</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Hora</label>
|
||||
<select id="m-hora-cita" class="time-select">
|
||||
<option value="">-- Seleccionar hora --</option>
|
||||
</div>
|
||||
|
||||
<!-- Descuentos y Anticipos -->
|
||||
<div class="discount-section modern-card">
|
||||
<div class="discount-header">
|
||||
<input type="checkbox" id="discount-toggle" class="modern-checkbox">
|
||||
<label for="discount-toggle" class="discount-label">
|
||||
<span class="discount-icon">🏷️</span>
|
||||
<span>Descuentos y Anticipos</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="discount-container" id="discount-container" style="display: none;">
|
||||
<div class="discount-options">
|
||||
<select id="discount-type" class="modern-select">
|
||||
<option value="">Sin descuento</option>
|
||||
<option value="percentage">💯 Porcentaje (%)</option>
|
||||
<option value="amount">💰 Cantidad fija ($)</option>
|
||||
<option value="anticipo">💳 Aplicar Anticipo</option>
|
||||
<option value="warrior">🎗️ Vanity (100%)</option>
|
||||
</select>
|
||||
<div class="discount-input-group">
|
||||
<input type="number" id="discount-value" min="0" step="0.01" placeholder="0" disabled />
|
||||
<span class="input-symbol" id="discount-symbol">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" id="discount-reason" placeholder="Motivo del descuento (opcional)" disabled class="modern-input" />
|
||||
|
||||
<!-- Checkbox de confirmación para anticipo manual -->
|
||||
<div id="anticipo-manual-confirmation" style="display: none; margin-top: 10px;">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" id="confirm-anticipo-manual">
|
||||
<span class="checkmark"></span>
|
||||
<span class="checkbox-text">✅ Confirmo que el anticipo existe y fue dado por la cliente</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anticipos Disponibles -->
|
||||
<div id="anticipos-section" class="anticipos-section modern-card" style="display: none;">
|
||||
<h4>💰 Anticipos Disponibles</h4>
|
||||
<div id="anticipos-disponibles" class="anticipos-list">
|
||||
<!-- Se llenan dinámicamente -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Datos de Cita -->
|
||||
<div class="appointment-section modern-card">
|
||||
<h4>📅 Datos de la Cita</h4>
|
||||
<div class="appointment-inputs">
|
||||
<input type="date" id="m-fecha-cita" class="modern-input" placeholder="Fecha" />
|
||||
<select id="m-hora-cita" class="modern-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>
|
||||
@@ -119,157 +251,108 @@
|
||||
<option value="17:30">5:30 PM</option>
|
||||
<option value="18:00">6:00 PM</option>
|
||||
</select>
|
||||
<div id="time-availability-info" class="time-availability-info" style="display: none;">
|
||||
<span id="available-slots-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resumen y Totales -->
|
||||
<div class="totals-section modern-card">
|
||||
<div class="totals-breakdown">
|
||||
<div class="total-row">
|
||||
<span>Subtotal:</span>
|
||||
<span id="subtotal-display">$0.00</span>
|
||||
</div>
|
||||
<div class="total-row discount-row" id="discount-display-row" style="display: none;">
|
||||
<span>Descuento:</span>
|
||||
<span id="discount-display" class="discount-amount">-$0.00</span>
|
||||
</div>
|
||||
<div class="total-row final-total">
|
||||
<span><strong>TOTAL:</strong></span>
|
||||
<span id="total-display" class="final-amount"><strong>$0.00</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Venta -->
|
||||
<div class="form-section">
|
||||
<h3>Venta</h3>
|
||||
<div class="products-container">
|
||||
<div class="product-selector">
|
||||
<select id="m-categoria" required>
|
||||
<option value="">-- Seleccione tipo --</option>
|
||||
<option value="service">Servicio</option>
|
||||
<option value="course">Curso</option>
|
||||
<option value="anticipo">Anticipo</option>
|
||||
</select>
|
||||
<select id="m-articulo" class="product-select">
|
||||
<option value="">-- Primero seleccione tipo --</option>
|
||||
</select>
|
||||
<input type="number" id="product-quantity" min="1" value="1" class="quantity-input" placeholder="Cant." />
|
||||
<button type="button" id="add-product-btn" class="btn-add">Agregar</button>
|
||||
</div>
|
||||
<div id="selected-products" class="selected-products"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Descuentos -->
|
||||
<div class="form-section discount-section">
|
||||
<div class="discount-header">
|
||||
<input type="checkbox" id="discount-toggle" class="discount-checkbox">
|
||||
<label for="discount-toggle" class="discount-label">
|
||||
<span class="material-icons-outlined discount-icon">percent</span>
|
||||
Aplicar descuento
|
||||
</label>
|
||||
</div>
|
||||
<div class="discount-container" id="discount-container" style="display: none;">
|
||||
<div class="discount-grid">
|
||||
<div class="form-group">
|
||||
<label>Tipo de descuento</label>
|
||||
<select id="discount-type">
|
||||
<option value="">Sin descuento</option>
|
||||
<option value="percentage">Porcentaje (%)</option>
|
||||
<option value="amount">Cantidad fija ($)</option>
|
||||
<option value="warrior">🎗️ Vanity (100%)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Valor del descuento</label>
|
||||
<div class="input-with-symbol">
|
||||
<input type="number" id="discount-value" min="0" step="0.01" placeholder="0" disabled />
|
||||
<span class="input-symbol" id="discount-symbol">%</span>
|
||||
<!-- Método de Pago -->
|
||||
<div class="payment-section modern-card">
|
||||
<h4>💳 Método de Pago</h4>
|
||||
<div class="payment-grid">
|
||||
<label class="payment-option">
|
||||
<input type="radio" name="m-metodo" value="Efectivo" checked />
|
||||
<div class="payment-card">
|
||||
<span class="payment-icon">💵</span>
|
||||
<span>Efectivo</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group full-width-discount">
|
||||
<label>Motivo del descuento</label>
|
||||
<input type="text" id="discount-reason" placeholder="Ej: Cliente frecuente, promoción especial..." disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div class="discount-preview" id="discount-preview" style="display: none;">
|
||||
<div class="discount-preview-item">
|
||||
<span>Descuento aplicado:</span>
|
||||
<span id="discount-preview-amount" class="discount-amount">$0.00</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="payment-option">
|
||||
<input type="radio" name="m-metodo" value="Tarjeta" />
|
||||
<div class="payment-card">
|
||||
<span class="payment-icon">💳</span>
|
||||
<span>Tarjeta</span>
|
||||
</div>
|
||||
</label>
|
||||
<label class="payment-option">
|
||||
<input type="radio" name="m-metodo" value="Transferencia" />
|
||||
<div class="payment-card">
|
||||
<span class="payment-icon">🏦</span>
|
||||
<span>Transferencia</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Anticipos Disponibles -->
|
||||
<div class="form-section anticipos-section" id="anticipos-section" style="display: none;">
|
||||
<h4>💰 Anticipos Disponibles</h4>
|
||||
<div id="anticipos-disponibles" class="anticipos-container">
|
||||
<!-- Los anticipos se cargarán dinámicamente -->
|
||||
<!-- Notas -->
|
||||
<div class="notes-section modern-card">
|
||||
<h4>📝 Notas Adicionales</h4>
|
||||
<textarea id="m-notas" placeholder="Comentarios especiales sobre la venta..." class="modern-textarea"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Botones de Acción -->
|
||||
<form id="formMove" class="checkout-form">
|
||||
<input type="hidden" id="m-monto" />
|
||||
<input type="hidden" id="m-concepto" />
|
||||
<input type="hidden" id="m-staff" />
|
||||
<input type="hidden" id="m-metodo" value="Efectivo" />
|
||||
<div class="action-buttons">
|
||||
<button type="submit" class="btn-checkout">
|
||||
<span class="btn-icon">🎫</span>
|
||||
<span>Generar Venta</span>
|
||||
</button>
|
||||
<button type="reset" class="btn-clear">
|
||||
<span class="btn-icon">🗑️</span>
|
||||
<span>Limpiar</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div> <!-- fin checkout-panel -->
|
||||
</div> <!-- fin sales-layout -->
|
||||
</div> <!-- fin sales-container -->
|
||||
</div> <!-- fin sub-tab-ventas -->
|
||||
|
||||
<!-- Sub-Pestaña de Tickets -->
|
||||
<div id="sub-tab-tickets" class="sub-tab-content">
|
||||
<div class="section">
|
||||
<h2>🎫 Historial de Tickets y Movimientos</h2>
|
||||
<button id="btnExport" class="btn-secondary">Exportar a CSV</button>
|
||||
<div class="table-wrapper">
|
||||
<table id="tblMoves">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Folio</th>
|
||||
<th>Fecha</th>
|
||||
<th>Cita</th>
|
||||
<th>Cliente</th>
|
||||
<th>Servicio</th>
|
||||
<th>Monto</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Método de Pago y Detalles -->
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Método de Pago *</label>
|
||||
<select id="m-metodo" required>
|
||||
<option value="">-- Seleccione método de pago --</option>
|
||||
<option value="Efectivo">Efectivo</option>
|
||||
<option value="Tarjeta">Tarjeta</option>
|
||||
<option value="Transferencia">Transferencia</option>
|
||||
<option value="Depósito">Depósito</option>
|
||||
<option value="Giftcard">Giftcard</option>
|
||||
<option value="Interno">Interno</option>
|
||||
<option value="Otros">Otros</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Atendió</label>
|
||||
<input type="text" id="m-staff" readonly />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group full-width">
|
||||
<label>Notas</label>
|
||||
<textarea id="m-notas" rows="2" placeholder="Notas adicionales..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Totales -->
|
||||
<div class="totals-section">
|
||||
<div class="totals-row">
|
||||
<span>Subtotal:</span>
|
||||
<span id="subtotal-display">$0.00</span>
|
||||
</div>
|
||||
<div class="totals-row" id="discount-display" style="display: none;">
|
||||
<span>Descuento:</span>
|
||||
<span id="discount-amount-display">-$0.00</span>
|
||||
</div>
|
||||
<div class="totals-row total-final">
|
||||
<span>Total:</span>
|
||||
<span id="total-display">$0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Campos ocultos para compatibilidad -->
|
||||
<input type="hidden" id="m-monto" />
|
||||
<input type="hidden" id="m-concepto" />
|
||||
|
||||
<div class="form-actions-modern">
|
||||
<button type="submit" class="btn-primary-large">Generar Venta y Ticket</button>
|
||||
<button type="reset" class="btn-secondary-large">Limpiar Formulario</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Movimientos Recientes</h2>
|
||||
<button id="btnExport" class="btn-secondary">Exportar a CSV</button>
|
||||
<div class="table-wrapper">
|
||||
<table id="tblMoves">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Folio</th>
|
||||
<th>Fecha</th>
|
||||
<th>Cita</th>
|
||||
<th>Cliente</th>
|
||||
<th>Servicio</th>
|
||||
<th>Monto</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- fin sub-tab-tickets -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Pestaña de Clientes -->
|
||||
@@ -438,6 +521,9 @@
|
||||
<div class="form-actions">
|
||||
<button type="submit">Guardar</button>
|
||||
<button type="reset" id="btnCancelEditProduct" class="btn-danger">Cancelar</button>
|
||||
<button type="button" id="btnImportProducts" class="btn-secondary" onclick="importProductsFromJSON()">
|
||||
📥 Importar Productos JSON
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -528,6 +614,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña de Solicitudes de Cancelación (Solo Admins) -->
|
||||
<div id="tab-cancellation-requests" class="tab-content">
|
||||
<div class="section">
|
||||
<h2>Solicitudes de Cancelación</h2>
|
||||
<p class="section-description">Aquí puedes revisar y gestionar las solicitudes de cancelación de ventas enviadas por los usuarios.</p>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table id="tblCancellationRequests">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Folio</th>
|
||||
<th>Cliente</th>
|
||||
<th>Monto</th>
|
||||
<th>Solicitado por</th>
|
||||
<th>Fecha Solicitud</th>
|
||||
<th>Motivo</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="no-cancellation-requests" style="display: none;">
|
||||
<p style="text-align: center; color: #6c757d; font-style: italic; margin-top: 2rem;">
|
||||
No hay solicitudes de cancelación pendientes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña de Configuración -->
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<div class="section">
|
||||
@@ -629,6 +747,6 @@
|
||||
<div id="printArea" class="no-print"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1/build/qrcode.min.js"></script>
|
||||
<script type="module" src="app.js?v=1757039801"></script>
|
||||
<script type="module" src="app.js?v=1757039803"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ap-pos",
|
||||
"version": "1.4.1",
|
||||
"version": "1.5.0",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
|
||||
36
print.js
36
print.js
@@ -97,7 +97,41 @@ function templateTicket(mov, settings) {
|
||||
|
||||
// DESCUENTOS SI EXISTEN
|
||||
if (mov.descuento && mov.descuento > 0) {
|
||||
lines.push(`<div class="t-row t-small"><span>Descuento ${mov.motivoDescuento ? '(' + esc(mov.motivoDescuento) + ')' : ''}:</span><span>-$${Number(mov.descuento).toFixed(2)}</span></div>`);
|
||||
if (mov.discountInfo) {
|
||||
// Mostrar información detallada del descuento
|
||||
const discountInfo = mov.discountInfo;
|
||||
let descriptionText = '';
|
||||
let amountText = `-$${Number(mov.descuento).toFixed(2)}`;
|
||||
|
||||
if (discountInfo.type === 'anticipo') {
|
||||
// Distinguir entre anticipo registrado y manual
|
||||
if (discountInfo.anticipo && discountInfo.anticipo.manual) {
|
||||
descriptionText = `Anticipo manual aplicado (${discountInfo.anticipo.comentario})`;
|
||||
} else {
|
||||
descriptionText = `Anticipo aplicado ${discountInfo.anticipo ? `(${discountInfo.anticipo.folio})` : ''}`;
|
||||
}
|
||||
amountText = `-$${Number(mov.descuento).toFixed(2)} (${discountInfo.percentage.toFixed(1)}%)`;
|
||||
} else if (discountInfo.type === 'warrior') {
|
||||
descriptionText = 'Descuento Vanity (100%)';
|
||||
amountText = `-$${Number(mov.descuento).toFixed(2)} (${discountInfo.percentage.toFixed(1)}%)`;
|
||||
} else if (discountInfo.type === 'percentage') {
|
||||
descriptionText = `Descuento (${discountInfo.value}%)`;
|
||||
amountText = `-$${Number(mov.descuento).toFixed(2)} (${discountInfo.percentage.toFixed(1)}%)`;
|
||||
} else if (discountInfo.type === 'amount') {
|
||||
descriptionText = `Descuento ($${discountInfo.value})`;
|
||||
amountText = `-$${Number(mov.descuento).toFixed(2)} (${discountInfo.percentage.toFixed(1)}%)`;
|
||||
}
|
||||
|
||||
lines.push(`<div class="t-row t-small"><span>${descriptionText}:</span><span>${amountText}</span></div>`);
|
||||
|
||||
// Mostrar comentario del descuento si existe
|
||||
if (discountInfo.reason && discountInfo.reason.trim()) {
|
||||
lines.push(`<div class="t-center t-small t-service-detail"><b>Motivo:</b> ${esc(discountInfo.reason)}</div>`);
|
||||
}
|
||||
} else {
|
||||
// Fallback para formato anterior
|
||||
lines.push(`<div class="t-row t-small"><span>Descuento ${mov.motivoDescuento ? '(' + esc(mov.motivoDescuento) + ')' : ''}:</span><span>-$${Number(mov.descuento).toFixed(2)}</span></div>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (mov.staff) lines.push(`<div class="t-center t-small t-service-detail"><b>Te atendió:</b> ${esc(mov.staff)}</div>`);
|
||||
|
||||
215
server.js
215
server.js
@@ -4,6 +4,7 @@ const sqlite3 = require('sqlite3').verbose();
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const app = express();
|
||||
const port = 3111;
|
||||
@@ -135,6 +136,55 @@ function initializeApplication() {
|
||||
FOREIGN KEY (course_id) REFERENCES products (id) ON DELETE CASCADE
|
||||
)`);
|
||||
|
||||
// --- Tabla de Solicitudes de Cancelación ---
|
||||
db.run(`CREATE TABLE IF NOT EXISTS cancellation_requests (
|
||||
id TEXT PRIMARY KEY,
|
||||
movement_id TEXT NOT NULL,
|
||||
requested_by TEXT NOT NULL, -- user id
|
||||
reason TEXT NOT NULL,
|
||||
status TEXT DEFAULT 'pending', -- 'pending', 'approved', 'denied'
|
||||
created_at TEXT NOT NULL,
|
||||
reviewed_by TEXT,
|
||||
reviewed_at TEXT,
|
||||
admin_notes TEXT,
|
||||
FOREIGN KEY (movement_id) REFERENCES movements (id),
|
||||
FOREIGN KEY (requested_by) REFERENCES users (id),
|
||||
FOREIGN KEY (reviewed_by) REFERENCES users (id)
|
||||
)`);
|
||||
|
||||
// Agregar columna de estado temporal a movements
|
||||
db.run("ALTER TABLE movements ADD COLUMN temp_cancelled INTEGER DEFAULT 0", (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error("Error adding temp_cancelled column:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Agregar columnas para categorización de productos
|
||||
db.run("ALTER TABLE products ADD COLUMN category TEXT", (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error("Error adding category column:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
db.run("ALTER TABLE products ADD COLUMN subcategory TEXT", (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error("Error adding subcategory column:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
db.run("ALTER TABLE products ADD COLUMN custom_price INTEGER DEFAULT 0", (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error("Error adding custom_price column:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Agregar campo de orden para control de secuencia
|
||||
db.run("ALTER TABLE products ADD COLUMN sort_order INTEGER DEFAULT 0", (err) => {
|
||||
if (err && !err.message.includes('duplicate column name')) {
|
||||
console.error("Error adding sort_order column:", err.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Una vez completada toda la inicialización de la DB, iniciar el servidor
|
||||
startServer();
|
||||
});
|
||||
@@ -318,7 +368,7 @@ function startServer() {
|
||||
|
||||
// --- Movements ---
|
||||
apiRouter.get('/movements', (req, res) => {
|
||||
db.all("SELECT * FROM movements ORDER BY fechaISO DESC", [], (err, rows) => {
|
||||
db.all("SELECT * FROM movements WHERE temp_cancelled = 0 OR temp_cancelled IS NULL ORDER BY fechaISO DESC", [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
});
|
||||
@@ -333,13 +383,107 @@ function startServer() {
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.delete('/movements/:id', (req, res) => {
|
||||
apiRouter.delete('/movements/:id', isAdmin, (req, res) => {
|
||||
db.run(`DELETE FROM movements WHERE id = ?`, req.params.id, function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json({ message: 'Movement deleted' });
|
||||
});
|
||||
});
|
||||
|
||||
// --- Cancellation Requests ---
|
||||
apiRouter.post('/movements/:id/cancel-request', (req, res) => {
|
||||
const { reason } = req.body;
|
||||
const movementId = req.params.id;
|
||||
|
||||
if (!reason || reason.trim().length === 0) {
|
||||
return res.status(400).json({ error: 'Reason is required' });
|
||||
}
|
||||
|
||||
const requestId = crypto.randomUUID();
|
||||
const createdAt = new Date().toISOString();
|
||||
|
||||
// First, check if movement exists and is not already cancelled
|
||||
db.get("SELECT * FROM movements WHERE id = ?", [movementId], (err, movement) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (!movement) return res.status(404).json({ error: 'Movement not found' });
|
||||
if (movement.temp_cancelled) return res.status(400).json({ error: 'Movement already has a pending cancellation request' });
|
||||
|
||||
// Check if there's already a pending request
|
||||
db.get("SELECT * FROM cancellation_requests WHERE movement_id = ? AND status = 'pending'", [movementId], (err, existingRequest) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (existingRequest) return res.status(400).json({ error: 'There is already a pending cancellation request for this sale' });
|
||||
|
||||
// Create cancellation request
|
||||
db.run(`INSERT INTO cancellation_requests (id, movement_id, requested_by, reason, status, created_at)
|
||||
VALUES (?, ?, ?, ?, 'pending', ?)`,
|
||||
[requestId, movementId, req.session.userId, reason.trim(), createdAt], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
|
||||
// Mark movement as temporarily cancelled
|
||||
db.run("UPDATE movements SET temp_cancelled = 1 WHERE id = ?", [movementId], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.status(201).json({
|
||||
message: 'Cancellation request created successfully',
|
||||
requestId: requestId
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.get('/cancellation-requests', isAdmin, (req, res) => {
|
||||
const sql = `
|
||||
SELECT cr.*, u.name as requested_by_name, m.folio, m.monto, m.concepto, c.nombre as client_name
|
||||
FROM cancellation_requests cr
|
||||
LEFT JOIN users u ON cr.requested_by = u.id
|
||||
LEFT JOIN movements m ON cr.movement_id = m.id
|
||||
LEFT JOIN clients c ON m.clienteId = c.id
|
||||
ORDER BY cr.created_at DESC
|
||||
`;
|
||||
db.all(sql, [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.put('/cancellation-requests/:id', isAdmin, (req, res) => {
|
||||
const { status, admin_notes } = req.body;
|
||||
const requestId = req.params.id;
|
||||
|
||||
if (!status || !['approved', 'denied'].includes(status)) {
|
||||
return res.status(400).json({ error: 'Invalid status. Must be approved or denied' });
|
||||
}
|
||||
|
||||
const reviewedAt = new Date().toISOString();
|
||||
|
||||
db.get("SELECT * FROM cancellation_requests WHERE id = ?", [requestId], (err, request) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
if (!request) return res.status(404).json({ error: 'Cancellation request not found' });
|
||||
if (request.status !== 'pending') return res.status(400).json({ error: 'Request already processed' });
|
||||
|
||||
// Update request status
|
||||
db.run(`UPDATE cancellation_requests SET status = ?, reviewed_by = ?, reviewed_at = ?, admin_notes = ? WHERE id = ?`,
|
||||
[status, req.session.userId, reviewedAt, admin_notes || '', requestId], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
|
||||
if (status === 'approved') {
|
||||
// If approved, delete the movement
|
||||
db.run("DELETE FROM movements WHERE id = ?", [request.movement_id], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json({ message: 'Cancellation approved and sale deleted' });
|
||||
});
|
||||
} else {
|
||||
// If denied, remove temporary cancellation
|
||||
db.run("UPDATE movements SET temp_cancelled = 0 WHERE id = ?", [request.movement_id], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json({ message: 'Cancellation request denied and sale restored' });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Client History ---
|
||||
apiRouter.get('/clients/:id/history', (req, res) => {
|
||||
db.all("SELECT * FROM movements WHERE clienteId = ? ORDER BY fechaISO DESC", [req.params.id], (err, rows) => {
|
||||
@@ -350,32 +494,69 @@ function startServer() {
|
||||
|
||||
// --- Product/Course Management ---
|
||||
apiRouter.get('/products', (req, res) => {
|
||||
db.all("SELECT * FROM products ORDER BY type, name", [], (err, rows) => {
|
||||
db.all("SELECT * FROM products ORDER BY type, sort_order, name", [], (err, rows) => {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json(rows);
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.post('/products', isAdmin, (req, res) => {
|
||||
const { name, type, price } = req.body;
|
||||
apiRouter.post('/products', (req, res) => {
|
||||
const { name, type, price, category, subcategory, custom_price } = req.body;
|
||||
if (!name || !type) return res.status(400).json({ error: 'Name and type are required' });
|
||||
db.run(`INSERT INTO products (name, type, price) VALUES (?, ?, ?)`,
|
||||
[name, type, price || 0], function(err) {
|
||||
db.run(`INSERT INTO products (name, type, price, category, subcategory, custom_price, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[name, type, price || 0, category, subcategory, custom_price ? 1 : 0, 0], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.status(201).json({ id: this.lastID, name, type, price });
|
||||
res.status(201).json({ id: this.lastID, name, type, price, category, subcategory, custom_price, sort_order: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.put('/products/:id', isAdmin, (req, res) => {
|
||||
const { name, type, price } = req.body;
|
||||
apiRouter.put('/products/:id', (req, res) => {
|
||||
const { name, type, price, category, subcategory, custom_price } = req.body;
|
||||
if (!name || !type) return res.status(400).json({ error: 'Name and type are required' });
|
||||
db.run(`UPDATE products SET name = ?, type = ?, price = ? WHERE id = ?`,
|
||||
[name, type, price || 0, req.params.id], function(err) {
|
||||
db.run(`UPDATE products SET name = ?, type = ?, price = ?, category = ?, subcategory = ?, custom_price = ? WHERE id = ?`,
|
||||
[name, type, price || 0, category, subcategory, custom_price ? 1 : 0, req.params.id], function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
res.json({ message: 'Product updated' });
|
||||
});
|
||||
});
|
||||
|
||||
// Bulk update products from JSON
|
||||
apiRouter.post('/products/bulk-import', isAdmin, (req, res) => {
|
||||
const { products } = req.body;
|
||||
if (!products || !Array.isArray(products)) {
|
||||
return res.status(400).json({ error: 'Products array is required' });
|
||||
}
|
||||
|
||||
db.serialize(() => {
|
||||
db.run("BEGIN TRANSACTION");
|
||||
|
||||
const stmt = db.prepare(`INSERT OR REPLACE INTO products
|
||||
(name, type, price, category, subcategory, custom_price, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
||||
|
||||
products.forEach(product => {
|
||||
stmt.run([
|
||||
product.name,
|
||||
product.type || 'service',
|
||||
product.price || null,
|
||||
product.category || null,
|
||||
product.subcategory || null,
|
||||
product.custom_price ? 1 : 0,
|
||||
product.sort_order || 0
|
||||
]);
|
||||
});
|
||||
|
||||
stmt.finalize();
|
||||
|
||||
db.run("COMMIT", function(err) {
|
||||
if (err) {
|
||||
db.run("ROLLBACK");
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
res.json({ message: 'Products imported successfully', count: products.length });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
apiRouter.delete('/products/:id', isAdmin, (req, res) => {
|
||||
db.run(`DELETE FROM products WHERE id = ?`, req.params.id, function(err) {
|
||||
if (err) return res.status(500).json({ error: err.message });
|
||||
@@ -490,15 +671,15 @@ function startServer() {
|
||||
// --- Dashboard Route (Authenticated Users) ---
|
||||
apiRouter.get('/dashboard', isAuthenticated, (req, res) => {
|
||||
const queries = {
|
||||
totalIncome: "SELECT SUM(monto) as total FROM movements",
|
||||
totalMovements: "SELECT COUNT(*) as total FROM movements",
|
||||
incomeByService: "SELECT tipo, SUM(monto) as total FROM movements GROUP BY tipo",
|
||||
incomeByPaymentMethod: "SELECT metodo, SUM(monto) as total FROM movements WHERE metodo IS NOT NULL AND metodo != '''' GROUP BY metodo",
|
||||
totalIncome: "SELECT SUM(monto) as total FROM movements WHERE temp_cancelled = 0 OR temp_cancelled IS NULL",
|
||||
totalMovements: "SELECT COUNT(*) as total FROM movements WHERE temp_cancelled = 0 OR temp_cancelled IS NULL",
|
||||
incomeByService: "SELECT tipo, SUM(monto) as total FROM movements WHERE temp_cancelled = 0 OR temp_cancelled IS NULL GROUP BY tipo",
|
||||
incomeByPaymentMethod: "SELECT metodo, SUM(monto) as total FROM movements WHERE (temp_cancelled = 0 OR temp_cancelled IS NULL) AND metodo IS NOT NULL AND metodo != '''' GROUP BY metodo",
|
||||
upcomingAppointments: `
|
||||
SELECT m.id, m.folio, m.fechaCita, m.horaCita, c.nombre as clienteNombre
|
||||
FROM movements m
|
||||
JOIN clients c ON m.clienteId = c.id
|
||||
WHERE m.fechaCita IS NOT NULL AND m.fechaCita >= date('now')
|
||||
WHERE (m.temp_cancelled = 0 OR m.temp_cancelled IS NULL) AND m.fechaCita IS NOT NULL AND m.fechaCita >= date('now')
|
||||
ORDER BY m.fechaCita ASC, m.horaCita ASC
|
||||
LIMIT 5`
|
||||
};
|
||||
|
||||
791
styles.css
791
styles.css
@@ -1460,4 +1460,795 @@ table tbody tr:hover {
|
||||
|
||||
.btn-aplicar-anticipo:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
/* --- Cancellation Request Styles --- */
|
||||
.modal {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 15% auto;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
color: #aaa;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 20px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close-button:hover,
|
||||
.close-button:focus {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.cancellation-info {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.cancellation-info p {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeaa7;
|
||||
}
|
||||
|
||||
.status-approved {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status-denied {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
border: 1px solid #ffc107;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background-color: #e0a800;
|
||||
border-color: #d39e00;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
border: 1px solid #28a745;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
.reason-cell {
|
||||
max-width: 200px;
|
||||
word-wrap: break-word;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.processed-info {
|
||||
font-style: italic;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.processed-info small {
|
||||
font-size: 10px;
|
||||
color: #868e96;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: #6c757d;
|
||||
margin-bottom: 20px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* --- Modern Sales Interface --- */
|
||||
.sales-container {
|
||||
padding: 0;
|
||||
max-width: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.sales-header {
|
||||
background: #000000 !important;
|
||||
background-image: none !important;
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 15px rgba(44, 62, 80, 0.2);
|
||||
}
|
||||
|
||||
.sales-header h2 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.sales-summary {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cart-count, .cart-total {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.sales-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 30px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
/* --- Products Panel --- */
|
||||
.products-panel {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.client-selector {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.modern-input {
|
||||
width: 100%;
|
||||
padding: 14px 20px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.modern-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
background: white;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.modern-input::placeholder {
|
||||
color: #a0a8b0;
|
||||
}
|
||||
|
||||
/* --- Categories --- */
|
||||
.categories-container h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.category-section {
|
||||
margin-bottom: 20px;
|
||||
border: 2px solid #f5f6fa;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.category-section:hover {
|
||||
border-color: #e9ecef;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.category-header {
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.category-header:hover {
|
||||
background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
font-size: 24px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.category-header h4 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 18px;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.category-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.category-section.collapsed .category-toggle {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 15px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.product-card {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.product-card:nth-child(even) {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.product-card:hover {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
|
||||
.product-card.selected {
|
||||
background: #e8f5e9;
|
||||
border-left: 3px solid #4caf50;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
color: #2c3e50;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
min-width: 90px;
|
||||
display: block;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.product-price.custom {
|
||||
color: #e74c3c;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.btn-select-product {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-select-product:hover {
|
||||
background: #34495e;
|
||||
}
|
||||
|
||||
/* Anticipos section */
|
||||
.anticipos-grid {
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.anticipo-form {
|
||||
background: white;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.anticipo-input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.anticipo-input-group input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#anticipo-amount {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#anticipo-comment {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-add-anticipo {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-add-anticipo:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
/* Estilos para checkbox de confirmación de anticipo manual */
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin: 10px 0;
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: #fff8dc;
|
||||
border: 1px solid #f0ad4e;
|
||||
}
|
||||
|
||||
.checkbox-container input[type="checkbox"] {
|
||||
margin-right: 8px;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 14px;
|
||||
color: #856404;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkmark {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.quantity-input {
|
||||
width: 60px;
|
||||
padding: 8px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* --- Checkout Panel --- */
|
||||
.checkout-panel {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 25px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.modern-card {
|
||||
background: #fafbfc;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modern-card:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
border-color: #d1d9e0;
|
||||
}
|
||||
|
||||
.modern-card h3, .modern-card h4 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #2c3e50;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* --- Cart Section --- */
|
||||
.cart-section h3 {
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cart-items {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.empty-cart {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
opacity: 0.5;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cart-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.cart-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.item-details {
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.item-price {
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.btn-remove-item {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* --- Discount Section --- */
|
||||
.modern-checkbox {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.discount-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.discount-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.modern-checkbox:checked + .discount-label .discount-icon {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.discount-options {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.modern-select {
|
||||
flex: 1;
|
||||
padding: 10px 12px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.discount-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: white;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.discount-input-group input {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 8px 10px;
|
||||
font-size: 14px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.discount-input-group .input-symbol {
|
||||
background: #f8f9fa;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #667eea;
|
||||
border-radius: 6px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* --- Payment Section --- */
|
||||
.payment-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.payment-option {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.payment-option input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.payment-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 15px 10px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 12px;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.payment-option input:checked + .payment-card {
|
||||
border-color: #28a745;
|
||||
background: linear-gradient(135deg, #f8fff9 0%, #e8f5e8 100%);
|
||||
}
|
||||
|
||||
.payment-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* --- Totals Section --- */
|
||||
.totals-breakdown {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.total-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.total-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.final-total {
|
||||
border-top: 2px solid #e9ecef;
|
||||
margin-top: 10px;
|
||||
padding-top: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.final-amount {
|
||||
color: #28a745;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.discount-amount {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* --- Action Buttons --- */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-checkout {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 25px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-checkout:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.btn-clear {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 20px;
|
||||
border-radius: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-clear:hover {
|
||||
background: #5a6268;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* --- Notes Section --- */
|
||||
.modern-textarea {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.modern-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
/* --- Responsive Design --- */
|
||||
@media (max-width: 1200px) {
|
||||
.sales-layout {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.checkout-panel {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sales-header {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.products-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.payment-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.discount-options {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user