mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
fix: Remove clear form button and improve UX
Major UI/UX improvements and bug fixes: • Fixed date formatting bug in ticket receipts (undefined dates) • Removed clear form button per user request • Added favicon integration across all pages (apple-touch-icon, favicon variants) • Implemented auto-collapse for product categories after adding items • Changed all ticket content alignment to left for better readability • Made product interface more compact to reduce scrolling • Enhanced date validation and formatting throughout the system Technical changes: - Fixed esc() function regex that was causing date corruption - Added collapseAllCategories() function for better UX - Updated cache-busting versions for proper browser refresh - Improved date handling with proper validation - Added comprehensive favicon support with web manifest 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
30
app.js
30
app.js
@@ -1,4 +1,4 @@
|
|||||||
import { renderTicketAndPrint } from './print.js?v=1757039803';
|
import { renderTicketAndPrint } from './print.js?v=1757454000';
|
||||||
|
|
||||||
// --- GLOBAL VARIABLES ---
|
// --- GLOBAL VARIABLES ---
|
||||||
const defaultSettings = {
|
const defaultSettings = {
|
||||||
@@ -43,7 +43,16 @@ function construirFechaCita() {
|
|||||||
|
|
||||||
// Convertir de formato ISO (YYYY-MM-DD) a formato DD/MM/YYYY
|
// Convertir de formato ISO (YYYY-MM-DD) a formato DD/MM/YYYY
|
||||||
const dateParts = fechaPicker.value.split('-');
|
const dateParts = fechaPicker.value.split('-');
|
||||||
return `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`;
|
if (dateParts.length !== 3) return '';
|
||||||
|
|
||||||
|
const year = dateParts[0];
|
||||||
|
const month = dateParts[1];
|
||||||
|
const day = dateParts[2];
|
||||||
|
|
||||||
|
// Validar que todas las partes existan
|
||||||
|
if (!year || !month || !day) return '';
|
||||||
|
|
||||||
|
return `${day.padStart(2, '0')}/${month.padStart(2, '0')}/${year}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar horarios disponibles basados en la fecha seleccionada
|
// Actualizar horarios disponibles basados en la fecha seleccionada
|
||||||
@@ -2312,6 +2321,19 @@ function toggleCategory(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collapseAllCategories() {
|
||||||
|
const categorySections = document.querySelectorAll('.category-section');
|
||||||
|
categorySections.forEach(section => {
|
||||||
|
const productsGrid = section.querySelector('.products-grid');
|
||||||
|
const toggle = section.querySelector('.category-toggle');
|
||||||
|
if (productsGrid && toggle) {
|
||||||
|
productsGrid.style.display = 'none';
|
||||||
|
toggle.textContent = '▶';
|
||||||
|
section.classList.add('collapsed');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadProductsByCategories() {
|
async function loadProductsByCategories() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/products');
|
const response = await fetch('/api/products');
|
||||||
@@ -2451,6 +2473,9 @@ function addProductToCart(productId) {
|
|||||||
|
|
||||||
updateCartDisplay();
|
updateCartDisplay();
|
||||||
calculateTotals();
|
calculateTotals();
|
||||||
|
|
||||||
|
// Auto-collapse all categories after adding product
|
||||||
|
collapseAllCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeProductFromCart(productId) {
|
function removeProductFromCart(productId) {
|
||||||
@@ -3169,4 +3194,5 @@ function addAnticipo() {
|
|||||||
alert(`✅ ANTICIPO AGREGADO\n\n${anticipoName}: $${amount.toFixed(2)}`);
|
alert(`✅ ANTICIPO AGREGADO\n\n${anticipoName}: $${amount.toFixed(2)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||||
|
|||||||
4
cookies.txt
Normal file
4
cookies.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
14
index.html
14
index.html
@@ -4,8 +4,12 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Ale Ponce | AlMa</title>
|
<title>Ale Ponce | AlMa</title>
|
||||||
<!-- Favicon SVG -->
|
<!-- Favicons -->
|
||||||
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23444' d='M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4V6h16v12zM6 10h2v4H6zm3 0h2v4H9zm3 0h2v4h-2zm3 0h2v4h-2z'/%3E%3C/svg%3E">
|
<link rel="apple-touch-icon" sizes="180x180" href="/src/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/src/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/src/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/src/favicon/site.webmanifest">
|
||||||
|
<link rel="icon" href="/src/favicon/favicon.ico">
|
||||||
<!-- Google Fonts & Icons -->
|
<!-- Google Fonts & Icons -->
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
@@ -338,10 +342,6 @@
|
|||||||
<span class="btn-icon">🎫</span>
|
<span class="btn-icon">🎫</span>
|
||||||
<span>Generar Venta</span>
|
<span>Generar Venta</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="reset" class="btn-clear">
|
|
||||||
<span class="btn-icon">🗑️</span>
|
|
||||||
<span>Limpiar</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -768,6 +768,6 @@
|
|||||||
<div id="printArea" class="no-print"></div>
|
<div id="printArea" class="no-print"></div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/qrcode@1/build/qrcode.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/qrcode@1/build/qrcode.min.js"></script>
|
||||||
<script type="module" src="app.js?v=1757039803"></script>
|
<script type="module" src="app.js?v=1757462000"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
876
index_backup.html
Normal file
876
index_backup.html
Normal file
@@ -0,0 +1,876 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Ale Ponce | AlMa</title>
|
||||||
|
<!-- Favicon SVG -->
|
||||||
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23444' d='M20 4H4c-1.11 0-1.99.89-1.99 2L2 18c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4V6h16v12zM6 10h2v4H6zm3 0h2v4H9zm3 0h2v4h-2zm3 0h2v4h-2z'/%3E%3C/svg%3E">
|
||||||
|
<!-- Google Fonts & Icons -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<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" />
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="container">
|
||||||
|
<header class="main-header">
|
||||||
|
<!-- Logo del negocio en lugar de texto -->
|
||||||
|
<img src="src/logo.png" alt="Ale Ponce" class="header-logo">
|
||||||
|
<nav class="tabs">
|
||||||
|
<button type="button" class="tab-link" data-tab="tab-dashboard">
|
||||||
|
<span class="material-icons-outlined">dashboard</span><span>Dashboard</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="tab-link" data-tab="tab-movements">
|
||||||
|
<span class="material-icons-outlined">receipt_long</span><span>Ventas</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="tab-link" data-tab="tab-clients">
|
||||||
|
<span class="material-icons-outlined">groups</span><span>Clientes</span>
|
||||||
|
</button>
|
||||||
|
<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>
|
||||||
|
</nav>
|
||||||
|
<button type="button" id="btnLogout" class="btn-icon btn-danger">
|
||||||
|
<span class="material-icons-outlined">logout</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Pestaña de Dashboard -->
|
||||||
|
<div id="tab-dashboard" class="tab-content">
|
||||||
|
<div class="section">
|
||||||
|
<h2>Dashboard</h2>
|
||||||
|
<div class="dashboard-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Ingresos Totales</h3>
|
||||||
|
<p id="stat-total-income">$0.00</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>Servicios Realizados</h3>
|
||||||
|
<p id="stat-total-movements">0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="dashboard-chart">
|
||||||
|
<h3>Ingresos por Servicio</h3>
|
||||||
|
<canvas id="incomeChart"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-chart">
|
||||||
|
<h3>Ingresos por Método de Pago</h3>
|
||||||
|
<canvas id="paymentMethodChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<h3>Próximas Citas</h3>
|
||||||
|
<div id="upcoming-appointments-list" class="appointments-list">
|
||||||
|
<!-- Las citas se renderizarán aquí -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pestaña de Movimientos/Recibos -->
|
||||||
|
<div id="tab-movements" class="tab-content">
|
||||||
|
<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" required autocomplete="off" placeholder="Buscar o crear cliente..." />
|
||||||
|
<datalist id="client-list"></datalist>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Categorías de Productos -->
|
||||||
|
<div class="categories-container">
|
||||||
|
<h3>Selecciona tus servicios</h3>
|
||||||
|
|
||||||
|
<!-- Pestañas -->
|
||||||
|
<div class="category-section" data-category="Pestañas">
|
||||||
|
<div class="category-header">
|
||||||
|
<span class="category-icon">👁️</span>
|
||||||
|
<h4>Pestañas</h4>
|
||||||
|
<button class="category-toggle">▼</button>
|
||||||
|
</div>
|
||||||
|
<div class="products-grid" id="pestanas-products">
|
||||||
|
<!-- Los productos se cargarán dinámicamente aquí -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Microblading -->
|
||||||
|
<div class="category-section" data-category="Microblading">
|
||||||
|
<div class="category-header">
|
||||||
|
<span class="category-icon">✏️</span>
|
||||||
|
<h4>Microblading & Cejas</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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" />
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- 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" />
|
||||||
|
<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 -->
|
||||||
|
|
||||||
|
<!-- Sección de Movimientos Recientes -->
|
||||||
|
<div class="section">
|
||||||
|
<h2>Movimientos Recientes</h2>
|
||||||
|
<button id="btnExport" class="btn-secondary">Exportar a CSV</button>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="tblMoves">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Fecha de la Cita</label>
|
||||||
|
<input type="date" id="m-fecha-cita" class="date-picker" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Hora</label>
|
||||||
|
<select id="m-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 id="time-availability-info" class="time-availability-info" style="display: none;">
|
||||||
|
<span id="available-slots-count"></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>
|
||||||
|
Descuentos y Anticipos
|
||||||
|
</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="anticipo">💰 Aplicar Anticipo</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>
|
||||||
|
</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>
|
||||||
|
</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 -->
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<!-- Pestaña de Clientes -->
|
||||||
|
<div id="tab-clients" class="tab-content">
|
||||||
|
<div class="sub-tabs">
|
||||||
|
<button type="button" class="sub-tab-link active" data-subtab="sub-tab-register">Registro de Cliente</button>
|
||||||
|
<button type="button" class="sub-tab-link" data-subtab="sub-tab-consult">Consulta de Clientes</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-Pestaña de Registro -->
|
||||||
|
<div id="sub-tab-register" class="sub-tab-content active">
|
||||||
|
<div class="section">
|
||||||
|
<h2>Registrar Nuevo Cliente</h2>
|
||||||
|
<form id="formClient">
|
||||||
|
<input type="hidden" id="c-id" />
|
||||||
|
<div class="form-grid-single">
|
||||||
|
<label for="c-nombre">Nombre:</label>
|
||||||
|
<input type="text" id="c-nombre" required />
|
||||||
|
|
||||||
|
<label for="c-telefono">Teléfono:</label>
|
||||||
|
<input type="tel" id="c-telefono" />
|
||||||
|
|
||||||
|
<label for="c-genero">Género:</label>
|
||||||
|
<select id="c-genero">
|
||||||
|
<option value="">-- Seleccionar --</option>
|
||||||
|
<option value="Mujer">Mujer</option>
|
||||||
|
<option value="Hombre">Hombre</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label for="c-cumple">Cumpleaños:</label>
|
||||||
|
<input type="date" id="c-cumple" />
|
||||||
|
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<input type="checkbox" id="c-consent" />
|
||||||
|
<label for="c-consent">¿Consentimiento médico informado completo?</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<input type="checkbox" id="c-pacienteOncologico" />
|
||||||
|
<label for="c-pacienteOncologico">🎗️ Paciente Oncológico</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Campos condicionales para paciente oncológico -->
|
||||||
|
<div id="oncologico-fields" class="sub-section hidden">
|
||||||
|
<h3>📋 Información Médica Oncológica</h3>
|
||||||
|
<div class="form-grid-single">
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<input type="checkbox" id="c-oncologoAprueba" />
|
||||||
|
<label for="c-oncologoAprueba">¿Oncólogo aprueba procedimiento?</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label for="c-nombreMedico">Nombre del Médico:</label>
|
||||||
|
<input type="text" id="c-nombreMedico" />
|
||||||
|
|
||||||
|
<label for="c-telefonoMedico">Teléfono del Médico:</label>
|
||||||
|
<input type="tel" id="c-telefonoMedico" />
|
||||||
|
|
||||||
|
<label for="c-cedulaMedico">Cédula Profesional:</label>
|
||||||
|
<input type="text" id="c-cedulaMedico" />
|
||||||
|
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<input type="checkbox" id="c-pruebaAprobacion" />
|
||||||
|
<label for="c-pruebaAprobacion">🎗️ Presenta autorización médica explícita firmada</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="data-location-info">
|
||||||
|
El consentimiento del médico debe ser presentado físicamente antes de la evaluación.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="client-courses-section" class="sub-section">
|
||||||
|
<h3>Cursos Registrados</h3>
|
||||||
|
<div id="client-courses-list"></div>
|
||||||
|
<button type="button" id="btnAddCourseToClient" class="btn-secondary">Registrar Curso</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions-single">
|
||||||
|
<button type="submit">Guardar Cliente</button>
|
||||||
|
<button type="reset" id="btnCancelEditClient" class="btn-danger">Limpiar</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub-Pestaña de Consulta -->
|
||||||
|
<div id="sub-tab-consult" class="sub-tab-content">
|
||||||
|
<div class="section">
|
||||||
|
<div class="consult-grid">
|
||||||
|
<div class="client-list-container">
|
||||||
|
<h2>Lista de Clientes</h2>
|
||||||
|
<input type="text" id="search-client" placeholder="Buscar por nombre o teléfono..." />
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="tblClients">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Teléfono</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="client-record-container" class="client-record-container">
|
||||||
|
<h2>Expediente del Cliente</h2>
|
||||||
|
<div id="client-record-content" class="hidden">
|
||||||
|
<div id="client-details"></div>
|
||||||
|
<h3>Historial de Servicios</h3>
|
||||||
|
<div id="client-history-table-container" class="table-wrapper">
|
||||||
|
<table id="client-history-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Folio</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Servicio</th>
|
||||||
|
<th>Monto</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<h3>Historial de Cursos</h3>
|
||||||
|
<div id="client-courses-history-container" class="table-wrapper">
|
||||||
|
<!-- Course history will be rendered here -->
|
||||||
|
</div>
|
||||||
|
<div class="form-actions-single">
|
||||||
|
<button id="btn-edit-client" class="btn-secondary">Editar Cliente</button>
|
||||||
|
<button id="btn-delete-client" class="btn-danger">Eliminar Cliente</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="client-record-placeholder">
|
||||||
|
<p>Selecciona un cliente de la lista para ver su expediente.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pestaña de Productos -->
|
||||||
|
<div id="tab-products" class="tab-content">
|
||||||
|
<div class="section">
|
||||||
|
<h2>Gestión de Productos, Servicios y Anticipos</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Formulario para añadir/editar -->
|
||||||
|
<div class="sub-section">
|
||||||
|
<h3>Añadir/Editar Producto</h3>
|
||||||
|
<form id="formProduct">
|
||||||
|
<input type="hidden" id="p-id" />
|
||||||
|
<div class="form-grid">
|
||||||
|
<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>
|
||||||
|
</select>
|
||||||
|
<label>Precio (MXN):</label>
|
||||||
|
<input type="number" id="p-price" step="0.01" min="0" placeholder="0.00" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<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()" style="display: none;">
|
||||||
|
📥 Importar Productos JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="section-divider">
|
||||||
|
|
||||||
|
<!-- Tabla unificada -->
|
||||||
|
<div class="sub-section">
|
||||||
|
<h3>Todos los Productos
|
||||||
|
<button id="filter-toggle-btn" class="filter-toggle-btn" title="Filtros">
|
||||||
|
<span class="material-icons-outlined">tune</span>
|
||||||
|
</button>
|
||||||
|
</h3>
|
||||||
|
<p class="table-note">
|
||||||
|
<strong>Nota:</strong> Esta tabla muestra solo productos reales (servicios y cursos).
|
||||||
|
Los anticipos se manejan únicamente en notas de ventas y en la app de citas.
|
||||||
|
</p>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="tblAllProducts">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="sortable" data-field="folio">
|
||||||
|
Folio <span class="sort-icon">↕</span>
|
||||||
|
</th>
|
||||||
|
<th class="sortable" data-field="fecha">
|
||||||
|
Fecha <span class="sort-icon">↕</span>
|
||||||
|
</th>
|
||||||
|
<th class="sortable" data-field="cita">
|
||||||
|
Cita <span class="sort-icon">↕</span>
|
||||||
|
</th>
|
||||||
|
<th class="sortable" data-field="descripcion">
|
||||||
|
Descripción <span class="sort-icon">↕</span>
|
||||||
|
</th>
|
||||||
|
<th class="sortable" data-field="categoria">
|
||||||
|
Categoría <span class="sort-icon">↕</span>
|
||||||
|
</th>
|
||||||
|
<th>Precio</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Modal -->
|
||||||
|
<div id="filter-modal" class="filter-modal" style="display: none;">
|
||||||
|
<div class="filter-modal-content">
|
||||||
|
<div class="filter-modal-header">
|
||||||
|
<h4>Filtrar por...</h4>
|
||||||
|
<button id="filter-modal-close" class="filter-modal-close">
|
||||||
|
<span class="material-icons-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="filter-modal-body">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Descripción</label>
|
||||||
|
<input type="text" id="filter-description" placeholder="Buscar por descripción...">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Tipo</label>
|
||||||
|
<select id="filter-category">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="service">Servicios</option>
|
||||||
|
<option value="course">Cursos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Fecha desde</label>
|
||||||
|
<input type="date" id="filter-date-from">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Fecha hasta</label>
|
||||||
|
<input type="date" id="filter-date-to">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Precio mínimo</label>
|
||||||
|
<input type="number" id="filter-price-min" placeholder="0.00" step="0.01">
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Precio máximo</label>
|
||||||
|
<input type="number" id="filter-price-max" placeholder="0.00" step="0.01">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</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">
|
||||||
|
<h2>Configuración del Negocio</h2>
|
||||||
|
<form id="formSettings">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label>Nombre del negocio:</label><input type="text" id="s-negocio" required />
|
||||||
|
<label>Eslogan (opcional):</label><input type="text" id="s-tagline" />
|
||||||
|
<label>Calle y Número:</label><input type="text" id="s-calle" placeholder="Ej: Av. Siempre Viva 123" />
|
||||||
|
<label>Colonia y C.P.:</label><input type="text" id="s-colonia-cp" placeholder="Ej: Centro, 25000" />
|
||||||
|
<label>Teléfono:</label><input type="text" id="s-tel" />
|
||||||
|
<label>RFC:</label><input type="text" id="s-rfc" />
|
||||||
|
<label>Leyenda pie de ticket:</label><input type="text" id="s-leyenda" />
|
||||||
|
<label>Prefijo de folio:</label><input type="text"id="s-folioPrefix" />
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Guardar Configuración</button>
|
||||||
|
<button type="button" id="btnTestTicket" class="btn-secondary">Probar Ticket</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="credentials-section">
|
||||||
|
<h2>Mis Credenciales</h2>
|
||||||
|
<form id="formCredentials">
|
||||||
|
<div class="form-grid">
|
||||||
|
<label>Mi Nombre:</label>
|
||||||
|
<input type="text" id="s-name" required />
|
||||||
|
<label>Mi Usuario:</label>
|
||||||
|
<input type="text" id="s-username" required />
|
||||||
|
<label>Nueva Contraseña:</label>
|
||||||
|
<input type="password" id="s-password" placeholder="Dejar en blanco para no cambiar" />
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Guardar Credenciales</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- Sección de Gestión de Usuarios (Solo para Admins) -->
|
||||||
|
<div class="section" id="user-management-section" style="display: none;">
|
||||||
|
<h2>Gestión de Usuarios</h2>
|
||||||
|
<div class="sub-section">
|
||||||
|
<h3>Añadir/Editar Usuario</h3>
|
||||||
|
<form id="formAddUser">
|
||||||
|
<input type="hidden" id="u-id" />
|
||||||
|
<div class="form-grid">
|
||||||
|
<label>Nombre:</label>
|
||||||
|
<input type="text" id="u-name" required />
|
||||||
|
<label>Usuario:</label>
|
||||||
|
<input type="text" id="u-username" required />
|
||||||
|
<label>Contraseña:</label>
|
||||||
|
<input type="password" id="u-password" placeholder="Dejar en blanco para no cambiar" />
|
||||||
|
<label>Rol:</label>
|
||||||
|
<select id="u-role">
|
||||||
|
<option value="user">Usuario (Ventas)</option>
|
||||||
|
<option value="admin">Administrador</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit">Guardar Usuario</button>
|
||||||
|
<button type="reset" id="btnCancelEditUser" class="btn-danger">Cancelar Edición</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="sub-section">
|
||||||
|
<h3>Usuarios Existentes</h3>
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table id="tblUsers">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Nombre</th>
|
||||||
|
<th>Usuario</th>
|
||||||
|
<th>Rol</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<!-- Los usuarios se insertarán aquí -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="db-location-section">
|
||||||
|
<h2>Ubicación y Exportación de Datos
|
||||||
|
<span class="material-icons-outlined info-icon" id="db-info-icon" style="display: none;">help_outline</span>
|
||||||
|
</h2>
|
||||||
|
<div id="db-instructions" class="data-location-info hidden">
|
||||||
|
<p>Toda la información de tu negocio (clientes, recibos, configuración) se guarda localmente. Para respaldar tus datos, simplemente copia el archivo correspondiente.</p>
|
||||||
|
<p>Para usuarios avanzados, la base de datos (<code>ap-pos.db</code>) se encuentra dentro del contenedor de la aplicación. Puedes acceder a ella usando los comandos estándar de Docker para copiar el archivo.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="main-footer">
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<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=1757039803"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Ale Ponce | AlMa</title>
|
<title>Ale Ponce | AlMa</title>
|
||||||
|
<!-- Favicons -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/src/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/src/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/src/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/src/favicon/site.webmanifest">
|
||||||
|
<link rel="icon" href="/src/favicon/favicon.ico">
|
||||||
<link rel="stylesheet" href="styles.css" />
|
<link rel="stylesheet" href="styles.css" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
|||||||
56
print.js
56
print.js
@@ -4,7 +4,7 @@
|
|||||||
* @returns {string} El string escapado.
|
* @returns {string} El string escapado.
|
||||||
*/
|
*/
|
||||||
function esc(str) {
|
function esc(str) {
|
||||||
return String(str || '').replace(/[&<>"'/]/g, c => ({
|
return String(str || '').replace(/[&<>"']/g, c => ({
|
||||||
"&": "&",
|
"&": "&",
|
||||||
"<": "<",
|
"<": "<",
|
||||||
">": ">",
|
">": ">",
|
||||||
@@ -68,30 +68,40 @@ function templateTicket(mov, settings) {
|
|||||||
|
|
||||||
lines.push('<div class="t-divider"></div>');
|
lines.push('<div class="t-divider"></div>');
|
||||||
|
|
||||||
// CLIENTE PRIMERO
|
|
||||||
if (mov.client) lines.push(`<div class="t-center t-small t-service-detail"><b>Cliente:</b> ${esc(mov.client.nombre)}</div>`);
|
|
||||||
|
|
||||||
// DATOS DE CITA PROMINENTES
|
// INFORMACIÓN COMPACTA DEL CLIENTE Y CITA
|
||||||
if (mov.fechaCita || mov.horaCita) {
|
if (mov.client) lines.push(`<div class="t-left t-small"><b>Cliente:</b> ${esc(mov.client.nombre)}</div>`);
|
||||||
|
|
||||||
lines.push('<div class="t-spacer"></div>');
|
lines.push('<div class="t-spacer"></div>');
|
||||||
if (mov.fechaCita) lines.push(`<div class="t-center t-small"><b>Fecha de Cita:</b> ${esc(mov.fechaCita)}</div>`);
|
|
||||||
if (mov.horaCita) lines.push(`<div class="t-center t-small"><b>Hora de Cita:</b> ${esc(mov.horaCita)}</div>`);
|
// INFO EN LÍNEAS COMPACTAS
|
||||||
|
if (mov.fechaCita && mov.horaCita) {
|
||||||
|
lines.push(`<div class="t-left t-small"><b>Cita:</b> ${esc(mov.fechaCita)} - ${esc(mov.horaCita)}</div>`);
|
||||||
|
} else {
|
||||||
|
if (mov.fechaCita) lines.push(`<div class="t-left t-small"><b>Fecha de Cita:</b> ${esc(mov.fechaCita)}</div>`);
|
||||||
|
if (mov.horaCita) lines.push(`<div class="t-left t-small"><b>Hora de Cita:</b> ${esc(mov.horaCita)}</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push('<div class="t-spacer"></div>');
|
lines.push('<div class="t-spacer"></div>');
|
||||||
lines.push(`<div class="t-row t-small"><span><b>Folio:</b></span><span>${esc(mov.folio)}</span></div>`);
|
lines.push(`<div class="t-left t-small"><b>Folio:</b> ${esc(mov.folio)}</div>`);
|
||||||
|
|
||||||
// Usar la función de fecha específica para tickets
|
|
||||||
const fechaFinal = generarFechaTicketFINAL();
|
// Fecha de Venta
|
||||||
console.log("FECHA GENERADA PARA TICKET:", fechaFinal);
|
const fechaMovimiento = new Date(mov.fechaISO);
|
||||||
lines.push(`<div class="t-row t-small"><span><b>Fecha de Venta:</b></span><span>${esc(fechaFinal)}</span></div>`);
|
const dia = fechaMovimiento.getDate().toString().padStart(2, '0');
|
||||||
|
const mes = (fechaMovimiento.getMonth() + 1).toString().padStart(2, '0');
|
||||||
|
const año = fechaMovimiento.getFullYear().toString();
|
||||||
|
const hora = fechaMovimiento.getHours().toString().padStart(2, '0');
|
||||||
|
const minutos = fechaMovimiento.getMinutes().toString().padStart(2, '0');
|
||||||
|
const fechaFinal = `${dia}/${mes}/${año} ${hora}:${minutos}`;
|
||||||
|
lines.push(`<div class="t-left t-small"><b>Fecha de Venta:</b> ${esc(fechaFinal)}</div>`);
|
||||||
|
|
||||||
lines.push('<div class="t-divider"></div>');
|
lines.push('<div class="t-divider"></div>');
|
||||||
lines.push(`<div class="t-center t-service-title t-bold">${esc(tipoServicio)}</div>`);
|
lines.push(`<div class="t-left t-service-title t-bold">${esc(tipoServicio)}</div>`);
|
||||||
|
|
||||||
// CONCEPTO CON PRECIO
|
// CONCEPTO CON PRECIO
|
||||||
if (mov.concepto) {
|
if (mov.concepto) {
|
||||||
lines.push(`<div class="t-center t-small t-service-detail"><b>Concepto:</b></div>`);
|
lines.push(`<div class="t-left t-small t-service-detail"><b>Concepto:</b></div>`);
|
||||||
lines.push(`<div class="t-row t-small"><span>${esc(mov.concepto)}</span><span>$${montoFormateado}</span></div>`);
|
lines.push(`<div class="t-row t-small"><span>${esc(mov.concepto)}</span><span>$${montoFormateado}</span></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +136,7 @@ function templateTicket(mov, settings) {
|
|||||||
|
|
||||||
// Mostrar comentario del descuento si existe
|
// Mostrar comentario del descuento si existe
|
||||||
if (discountInfo.reason && discountInfo.reason.trim()) {
|
if (discountInfo.reason && discountInfo.reason.trim()) {
|
||||||
lines.push(`<div class="t-center t-small t-service-detail"><b>Motivo:</b> ${esc(discountInfo.reason)}</div>`);
|
lines.push(`<div class="t-left t-small t-service-detail"><b>Motivo:</b> ${esc(discountInfo.reason)}</div>`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback para formato anterior
|
// Fallback para formato anterior
|
||||||
@@ -134,16 +144,18 @@ function templateTicket(mov, settings) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mov.staff) lines.push(`<div class="t-center t-small t-service-detail"><b>Te atendió:</b> ${esc(mov.staff)}</div>`);
|
if (mov.notas) lines.push(`<div class="t-left t-small t-service-detail"><b>Notas:</b> ${esc(mov.notas)}</div>`);
|
||||||
if (mov.notas) lines.push(`<div class="t-center t-small t-service-detail"><b>Notas:</b> ${esc(mov.notas)}</div>`);
|
|
||||||
|
|
||||||
lines.push('<div class="t-divider"></div>');
|
lines.push('<div class="t-divider"></div>');
|
||||||
|
|
||||||
// TOTAL ALINEADO A LA DERECHA
|
// TOTAL ALINEADO A LA IZQUIERDA
|
||||||
lines.push(`<div class="t-row t-bold"><span></span><span><b>Total: $${montoFormateado}</b></span></div>`);
|
lines.push(`<div class="t-left t-bold t-total-large"><b>Total: $${montoFormateado}</b></div>`);
|
||||||
|
|
||||||
// MÉTODO DE PAGO DEBAJO DEL TOTAL - ALINEADO A LA DERECHA
|
// MÉTODO DE PAGO DEBAJO DEL TOTAL - ALINEADO A LA IZQUIERDA
|
||||||
if (mov.metodo) lines.push(`<div class="t-right t-small"><b>Método:</b> ${esc(mov.metodo)}</div>`);
|
if (mov.metodo) lines.push(`<div class="t-left t-small"><b>Método:</b> ${esc(mov.metodo)}</div>`);
|
||||||
|
|
||||||
|
// TE ATENDIÓ DEBAJO DEL MÉTODO DE PAGO
|
||||||
|
if (mov.staff) lines.push(`<div class="t-left t-small"><b>Te atendió:</b> ${esc(mov.staff)}</div>`);
|
||||||
|
|
||||||
if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) {
|
if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) {
|
||||||
lines.push('<div class="t-divider"></div>');
|
lines.push('<div class="t-divider"></div>');
|
||||||
@@ -236,4 +248,4 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// FORZAR RECARGA - 2025-09-05T20:43:00 - NUEVA FUNCIÓN generarFechaTicketFINAL
|
// FORZAR RECARGA - 2025-09-09T21:33:00 - TODO ALINEADO A LA IZQUIERDA
|
||||||
@@ -4,6 +4,12 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Configuración Inicial - AP POS</title>
|
<title>Configuración Inicial - AP POS</title>
|
||||||
|
<!-- Favicons -->
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/src/favicon/apple-touch-icon.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/src/favicon/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/src/favicon/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/src/favicon/site.webmanifest">
|
||||||
|
<link rel="icon" href="/src/favicon/favicon.ico">
|
||||||
<link rel="stylesheet" href="styles.css">
|
<link rel="stylesheet" href="styles.css">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
|
|||||||
BIN
src/favicon/android-chrome-192x192.png
Normal file
BIN
src/favicon/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/favicon/android-chrome-512x512.png
Normal file
BIN
src/favicon/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
BIN
src/favicon/apple-touch-icon.png
Normal file
BIN
src/favicon/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/favicon/favicon-16x16.png
Normal file
BIN
src/favicon/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 512 B |
BIN
src/favicon/favicon-32x32.png
Normal file
BIN
src/favicon/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/favicon/favicon.ico
Normal file
BIN
src/favicon/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
src/favicon/site.webmanifest
Normal file
1
src/favicon/site.webmanifest
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||||
52
styles.css
52
styles.css
@@ -440,7 +440,7 @@ th[onclick]:hover {
|
|||||||
.products-container {
|
.products-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-selector {
|
.product-selector {
|
||||||
@@ -887,6 +887,7 @@ button.action-btn {
|
|||||||
}
|
}
|
||||||
.t-center { text-align: center; }
|
.t-center { text-align: center; }
|
||||||
.t-right { text-align: right; }
|
.t-right { text-align: right; }
|
||||||
|
.t-left { text-align: left; }
|
||||||
.t-bold { font-weight: bold; }
|
.t-bold { font-weight: bold; }
|
||||||
.t-business-name { font-size: 14px; margin-bottom: 4px; }
|
.t-business-name { font-size: 14px; margin-bottom: 4px; }
|
||||||
.t-tagline { font-size: 11px; margin-bottom: 8px; font-style: italic; }
|
.t-tagline { font-size: 11px; margin-bottom: 8px; font-style: italic; }
|
||||||
@@ -899,6 +900,7 @@ button.action-btn {
|
|||||||
.t-row-label { font-weight: bold; }
|
.t-row-label { font-weight: bold; }
|
||||||
.t-service-detail-label { font-weight: bold; }
|
.t-service-detail-label { font-weight: bold; }
|
||||||
.t-footer { margin-top: 10px; }
|
.t-footer { margin-top: 10px; }
|
||||||
|
.t-total-large { font-size: 13px; }
|
||||||
|
|
||||||
.t-qr-section {
|
.t-qr-section {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
@@ -914,6 +916,27 @@ button.action-btn {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Estilos para columnas del ticket */
|
||||||
|
.t-client-appointment-section {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-client-appointment-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-client-appointment-col {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.t-client-appointment-col div {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/***** MODO IMPRESIÓN *****/
|
/***** MODO IMPRESIÓN *****/
|
||||||
@media print {
|
@media print {
|
||||||
@@ -1732,10 +1755,10 @@ table tbody tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.products-grid {
|
.products-grid {
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 15px;
|
gap: 10px;
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1743,12 +1766,13 @@ table tbody tr:hover {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto auto auto;
|
grid-template-columns: 1fr auto auto auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
padding: 12px 16px;
|
padding: 8px 12px;
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
background: white;
|
background: white;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-card:nth-child(even) {
|
.product-card:nth-child(even) {
|
||||||
@@ -1795,7 +1819,7 @@ table tbody tr:hover {
|
|||||||
|
|
||||||
.product-actions {
|
.product-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-self: end;
|
justify-self: end;
|
||||||
}
|
}
|
||||||
@@ -1804,9 +1828,9 @@ table tbody tr:hover {
|
|||||||
background: #2c3e50;
|
background: #2c3e50;
|
||||||
color: white;
|
color: white;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 6px 12px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
@@ -1895,12 +1919,12 @@ table tbody tr:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quantity-input {
|
.quantity-input {
|
||||||
width: 60px;
|
width: 45px;
|
||||||
padding: 8px;
|
padding: 4px;
|
||||||
border: 2px solid #e1e5e9;
|
border: 1px solid #e1e5e9;
|
||||||
border-radius: 6px;
|
border-radius: 3px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Checkout Panel --- */
|
/* --- Checkout Panel --- */
|
||||||
|
|||||||
Reference in New Issue
Block a user