mirror of
https://github.com/marcogll/ap_pos.git
synced 2026-01-13 13:15:16 +00:00
fix: Resolve ticket date formatting issues and enhance appointment system
- Fix ticket date format from "04undefined09undefined2025" to proper "DD/MM/YYYY HH:MM" - Implement proper date handling using movement's fechaISO timestamp - Add bold formatting for Folio and Fecha labels in tickets - Enhance appointment date picker with HTML5 date input - Implement smart time slot availability checking - Improve anticipo (advance payment) handling with better UX - Add comprehensive filtering system for products table - Update cache busting to v=99.9 for proper browser reload - Modernize date/time components throughout the application 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
449
app.js
449
app.js
@@ -1,5 +1,5 @@
|
||||
import { load, save, remove, KEY_DATA, KEY_SETTINGS, KEY_CLIENTS } from './storage.js';
|
||||
import { renderTicketAndPrint } from './print.js?v=1.8';
|
||||
import { renderTicketAndPrint } from './print.js?v=99.9';
|
||||
|
||||
// --- UTILITIES ---
|
||||
function escapeHTML(str) {
|
||||
@@ -15,20 +15,104 @@ function escapeHTML(str) {
|
||||
}
|
||||
|
||||
function construirFechaCita() {
|
||||
const dia = document.getElementById('m-cita-dia').value;
|
||||
const mes = document.getElementById('m-cita-mes').value;
|
||||
const año = document.getElementById('m-cita-año').value;
|
||||
const fechaPicker = document.getElementById('m-fecha-cita');
|
||||
if (!fechaPicker || !fechaPicker.value) return '';
|
||||
|
||||
if (!dia || !mes || !año) {
|
||||
return '';
|
||||
// Convertir de formato ISO (YYYY-MM-DD) a formato DD/MM/YYYY
|
||||
const dateParts = fechaPicker.value.split('-');
|
||||
return `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`;
|
||||
}
|
||||
|
||||
// Actualizar horarios disponibles basados en la fecha seleccionada
|
||||
function updateAvailableTimeSlots(selectedDate) {
|
||||
const horaSelect = document.getElementById('m-hora-cita');
|
||||
if (!horaSelect || !selectedDate) return;
|
||||
|
||||
// Horarios base disponibles
|
||||
const baseTimeSlots = [
|
||||
{ value: '10:00', label: '10:00 AM' },
|
||||
{ value: '10:30', label: '10:30 AM' },
|
||||
{ value: '11:00', label: '11:00 AM' },
|
||||
{ value: '11:30', label: '11:30 AM' },
|
||||
{ value: '12:00', label: '12:00 PM' },
|
||||
{ value: '12:30', label: '12:30 PM' },
|
||||
{ value: '13:00', label: '1:00 PM' },
|
||||
{ value: '13:30', label: '1:30 PM' },
|
||||
{ value: '14:00', label: '2:00 PM' },
|
||||
{ value: '14:30', label: '2:30 PM' },
|
||||
{ value: '15:00', label: '3:00 PM' },
|
||||
{ value: '15:30', label: '3:30 PM' },
|
||||
{ value: '16:00', label: '4:00 PM' },
|
||||
{ value: '16:30', label: '4:30 PM' },
|
||||
{ value: '17:00', label: '5:00 PM' },
|
||||
{ value: '17:30', label: '5:30 PM' },
|
||||
{ value: '18:00', label: '6:00 PM' }
|
||||
];
|
||||
|
||||
// Convertir fecha del date picker (YYYY-MM-DD) al formato usado en la base (DD/MM/YYYY)
|
||||
const dateParts = selectedDate.split('-');
|
||||
const formattedDate = `${dateParts[2]}/${dateParts[1]}/${dateParts[0]}`;
|
||||
|
||||
// Obtener citas ya programadas para esta fecha
|
||||
const existingAppointments = movements
|
||||
.filter(mov => {
|
||||
// Comparar tanto con formato ISO como DD/MM/YYYY
|
||||
return mov.fechaCita === selectedDate || mov.fechaCita === formattedDate;
|
||||
})
|
||||
.map(mov => mov.horaCita)
|
||||
.filter(hora => hora);
|
||||
|
||||
// Filtrar horarios disponibles
|
||||
const availableSlots = baseTimeSlots.filter(slot =>
|
||||
!existingAppointments.includes(slot.value)
|
||||
);
|
||||
|
||||
// Limpiar y repoblar selector
|
||||
const currentValue = horaSelect.value;
|
||||
horaSelect.innerHTML = '<option value="">-- Seleccionar hora --</option>';
|
||||
|
||||
availableSlots.forEach(slot => {
|
||||
const option = document.createElement('option');
|
||||
option.value = slot.value;
|
||||
option.textContent = slot.label;
|
||||
horaSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// Si había un horario ocupado seleccionado, mostrarlo como no disponible
|
||||
if (currentValue && existingAppointments.includes(currentValue)) {
|
||||
const busyOption = document.createElement('option');
|
||||
busyOption.value = currentValue;
|
||||
busyOption.textContent = `${currentValue} (Ocupado)`;
|
||||
busyOption.disabled = true;
|
||||
busyOption.style.color = '#dc3545';
|
||||
horaSelect.appendChild(busyOption);
|
||||
horaSelect.value = currentValue;
|
||||
}
|
||||
|
||||
// Formatear con ceros a la izquierda
|
||||
const diaStr = dia.padStart(2, '0');
|
||||
const mesStr = mes.padStart(2, '0');
|
||||
// Mostrar contador de horarios disponibles
|
||||
const availableCount = availableSlots.length;
|
||||
const totalCount = baseTimeSlots.length;
|
||||
console.log(`Horarios disponibles para ${selectedDate}: ${availableCount}/${totalCount}`);
|
||||
|
||||
// Retornar en formato YYYY-MM-DD para compatibilidad
|
||||
return `${año}-${mesStr}-${diaStr}`;
|
||||
// Actualizar indicador visual de disponibilidad
|
||||
const availabilityInfo = document.getElementById('time-availability-info');
|
||||
const availabilityCount = document.getElementById('available-slots-count');
|
||||
|
||||
if (availabilityInfo && availabilityCount) {
|
||||
if (availableCount > 0) {
|
||||
availabilityCount.textContent = `✅ ${availableCount} de ${totalCount} horarios disponibles`;
|
||||
availabilityInfo.style.display = 'block';
|
||||
availabilityInfo.style.backgroundColor = availableCount > totalCount * 0.5 ? '#e8f5e8' : '#fff3cd';
|
||||
availabilityInfo.style.borderColor = availableCount > totalCount * 0.5 ? '#c3e6cb' : '#ffeaa7';
|
||||
availabilityInfo.style.color = availableCount > totalCount * 0.5 ? '#155724' : '#856404';
|
||||
} else {
|
||||
availabilityCount.textContent = '❌ No hay horarios disponibles para esta fecha';
|
||||
availabilityInfo.style.display = 'block';
|
||||
availabilityInfo.style.backgroundColor = '#f8d7da';
|
||||
availabilityInfo.style.borderColor = '#f5c6cb';
|
||||
availabilityInfo.style.color = '#721c24';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sistema dinámico de productos y descuentos
|
||||
@@ -210,36 +294,8 @@ function addCurrentProduct() {
|
||||
|
||||
// Manejar anticipos de forma especial
|
||||
if (categoriaSelect.value === 'anticipo') {
|
||||
let anticipoAmount = prompt('Ingresa el monto del anticipo:', '');
|
||||
if (anticipoAmount === null) return; // Usuario canceló
|
||||
|
||||
anticipoAmount = parseFloat(anticipoAmount);
|
||||
if (isNaN(anticipoAmount) || anticipoAmount <= 0) {
|
||||
alert('Por favor ingresa un monto válido para el anticipo');
|
||||
return;
|
||||
}
|
||||
|
||||
const clienteInput = document.getElementById('m-cliente');
|
||||
const clienteName = clienteInput.value.trim();
|
||||
let anticipoName = 'Anticipo';
|
||||
if (clienteName) {
|
||||
anticipoName = `Anticipo - ${clienteName}`;
|
||||
}
|
||||
|
||||
const existingIndex = selectedProducts.findIndex(p => p.name === anticipoName);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
selectedProducts[existingIndex].quantity += quantity;
|
||||
selectedProducts[existingIndex].price += anticipoAmount; // Acumular el monto
|
||||
} else {
|
||||
selectedProducts.push({
|
||||
id: 'anticipo-' + Date.now(),
|
||||
name: anticipoName,
|
||||
price: anticipoAmount,
|
||||
quantity: quantity,
|
||||
type: 'anticipo'
|
||||
});
|
||||
}
|
||||
handleAnticipoSelection();
|
||||
return;
|
||||
} else {
|
||||
// Manejar servicios y cursos como antes
|
||||
const productData = products.find(p => p.name === articuloSelect.value && p.type === categoriaSelect.value);
|
||||
@@ -288,14 +344,19 @@ function renderSelectedProducts() {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = selectedProducts.map(product => `
|
||||
<div class="product-item">
|
||||
<span class="product-item-name">${escapeHTML(product.name)} <small>(${product.type === 'service' ? 'Servicio' : 'Curso'})</small></span>
|
||||
<span class="product-item-quantity">${product.quantity}x</span>
|
||||
<span class="product-item-price">$${(product.price * product.quantity).toFixed(2)}</span>
|
||||
<button type="button" class="btn-remove" onclick="removeProduct('${escapeHTML(product.name)}')">×</button>
|
||||
</div>
|
||||
`).join('');
|
||||
const html = selectedProducts.map(product => {
|
||||
// Para anticipos no agregar el tipo en small porque ya está en el nombre
|
||||
const typeLabel = product.type === 'anticipo' ? '' : ` <small>(${product.type === 'service' ? 'Servicio' : 'Curso'})</small>`;
|
||||
|
||||
return `
|
||||
<div class="product-item">
|
||||
<span class="product-item-name">${escapeHTML(product.name)}${typeLabel}</span>
|
||||
<span class="product-item-quantity">${product.quantity}x</span>
|
||||
<span class="product-item-price">$${(product.price * product.quantity).toFixed(2)}</span>
|
||||
<button type="button" class="btn-remove" onclick="removeProduct('${escapeHTML(product.name)}')">×</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
@@ -864,10 +925,10 @@ function populateArticuloDropdown(category) {
|
||||
articuloSelect.innerHTML = `<option value="">-- Seleccionar ${placeholder} --</option>`;
|
||||
|
||||
if (category === 'anticipo') {
|
||||
// Para anticipos, permitir búsqueda automática o ingreso manual
|
||||
// Para anticipos, solo una opción para ingresar monto
|
||||
const option = document.createElement('option');
|
||||
option.value = 'Anticipo';
|
||||
option.textContent = 'Anticipo - $0.00 (Ingreso manual)';
|
||||
option.textContent = 'Anticipo (Monto personalizado)';
|
||||
articuloSelect.appendChild(option);
|
||||
} else {
|
||||
const items = products.filter(p => p.type === category);
|
||||
@@ -1723,6 +1784,14 @@ async function initializeApp() {
|
||||
showAddCourseModal(clientId);
|
||||
});
|
||||
|
||||
// Event listener para el date picker de citas
|
||||
const fechaCitaPicker = document.getElementById('m-fecha-cita');
|
||||
if (fechaCitaPicker) {
|
||||
fechaCitaPicker.addEventListener('change', function() {
|
||||
updateAvailableTimeSlots(this.value);
|
||||
});
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
fetch('/api/settings').then(res => res.json()).catch(() => DEFAULT_SETTINGS),
|
||||
fetch('/api/movements').then(res => res.json()).catch(() => []),
|
||||
@@ -1788,29 +1857,14 @@ function generateProductFolio(type) {
|
||||
const timestamp = Date.now().toString().slice(-6);
|
||||
const typeCode = {
|
||||
'service': 'SRV',
|
||||
'course': 'CRS',
|
||||
'anticipo': 'ANT'
|
||||
'course': 'CRS'
|
||||
};
|
||||
return `${folioPrefix}-${typeCode[type]}-${timestamp}`;
|
||||
}
|
||||
|
||||
// Construir fecha de cita para anticipos
|
||||
function construirFechaCitaProducto() {
|
||||
const dia = document.getElementById('p-cita-dia').value;
|
||||
const mes = document.getElementById('p-cita-mes').value;
|
||||
const año = document.getElementById('p-cita-año').value;
|
||||
const hora = document.getElementById('p-hora-cita').value;
|
||||
|
||||
if (!dia || !mes || !año) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const diaStr = dia.padStart(2, '0');
|
||||
const mesStr = mes.padStart(2, '0');
|
||||
const horaStr = hora || '00:00';
|
||||
|
||||
return `${diaStr}/${mesStr}/${año} ${horaStr}`;
|
||||
}
|
||||
// Los anticipos ya no son productos - se manejan solo en ventas
|
||||
|
||||
// Los anticipos usan prompts nativos mejorados con emojis
|
||||
|
||||
// Renderizar tabla unificada
|
||||
function renderUnifiedProductsTable() {
|
||||
@@ -1823,18 +1877,57 @@ function renderUnifiedProductsTable() {
|
||||
// Aplicar filtros
|
||||
let filteredData = [...allProductsData];
|
||||
|
||||
const filterType = document.getElementById('filter-type')?.value;
|
||||
if (filterType) {
|
||||
filteredData = filteredData.filter(item => item.categoria === filterType);
|
||||
}
|
||||
|
||||
const searchTerm = document.getElementById('search-products')?.value?.toLowerCase();
|
||||
if (searchTerm) {
|
||||
// Filtro por descripción
|
||||
const filterDescription = document.getElementById('filter-description')?.value?.toLowerCase();
|
||||
if (filterDescription) {
|
||||
filteredData = filteredData.filter(item =>
|
||||
item.descripcion.toLowerCase().includes(searchTerm) ||
|
||||
item.categoria.toLowerCase().includes(searchTerm)
|
||||
item.descripcion.toLowerCase().includes(filterDescription)
|
||||
);
|
||||
}
|
||||
|
||||
// Filtro por tipo de producto (servicios y cursos únicamente)
|
||||
const filterCategory = document.getElementById('filter-category')?.value;
|
||||
if (filterCategory) {
|
||||
filteredData = filteredData.filter(item => item.categoria === filterCategory);
|
||||
}
|
||||
|
||||
// Filtro por rango de fechas
|
||||
const filterDateFrom = document.getElementById('filter-date-from')?.value;
|
||||
const filterDateTo = document.getElementById('filter-date-to')?.value;
|
||||
if (filterDateFrom || filterDateTo) {
|
||||
filteredData = filteredData.filter(item => {
|
||||
if (!item.fecha || item.fecha === 'N/A') return true;
|
||||
|
||||
// Convertir fecha del item a formato comparable
|
||||
const itemDate = new Date(item.fecha.split('/').reverse().join('-'));
|
||||
|
||||
if (filterDateFrom) {
|
||||
const fromDate = new Date(filterDateFrom);
|
||||
if (itemDate < fromDate) return false;
|
||||
}
|
||||
|
||||
if (filterDateTo) {
|
||||
const toDate = new Date(filterDateTo);
|
||||
if (itemDate > toDate) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Filtro por rango de precios
|
||||
const filterPriceMin = document.getElementById('filter-price-min')?.value;
|
||||
const filterPriceMax = document.getElementById('filter-price-max')?.value;
|
||||
if (filterPriceMin || filterPriceMax) {
|
||||
filteredData = filteredData.filter(item => {
|
||||
const price = parseFloat(item.precio) || 0;
|
||||
|
||||
if (filterPriceMin && price < parseFloat(filterPriceMin)) return false;
|
||||
if (filterPriceMax && price > parseFloat(filterPriceMax)) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Ordenar datos
|
||||
filteredData.sort((a, b) => {
|
||||
@@ -1890,8 +1983,7 @@ function renderUnifiedProductsTable() {
|
||||
function getCategoryName(categoria) {
|
||||
const names = {
|
||||
'service': 'Servicio',
|
||||
'course': 'Curso',
|
||||
'anticipo': 'Anticipo'
|
||||
'course': 'Curso'
|
||||
};
|
||||
return names[categoria] || categoria;
|
||||
}
|
||||
@@ -1900,7 +1992,8 @@ function getCategoryName(categoria) {
|
||||
function loadUnifiedProductsData() {
|
||||
allProductsData = [];
|
||||
|
||||
// Agregar productos existentes (servicios y cursos)
|
||||
// Agregar solo productos existentes (servicios y cursos)
|
||||
// Los anticipos NO se incluyen aquí - solo se manejan en ventas/notas
|
||||
products.forEach(product => {
|
||||
allProductsData.push({
|
||||
id: product.id,
|
||||
@@ -1913,24 +2006,6 @@ function loadUnifiedProductsData() {
|
||||
status: product.status || 'active'
|
||||
});
|
||||
});
|
||||
|
||||
// Agregar anticipos desde movimientos
|
||||
const anticiposMovements = movements.filter(m =>
|
||||
m.concepto && (m.concepto.includes('Anticipo') || m.concepto.includes('anticipo'))
|
||||
);
|
||||
|
||||
anticiposMovements.forEach(anticipo => {
|
||||
allProductsData.push({
|
||||
id: `anticipo-${anticipo.id}`,
|
||||
folio: anticipo.folio,
|
||||
fecha: new Date(anticipo.fecha).toLocaleDateString('es-ES'),
|
||||
cita: anticipo.fechaCita ? new Date(anticipo.fechaCita).toLocaleDateString('es-ES') + ' ' + (anticipo.horaCita || '') : 'N/A',
|
||||
descripcion: anticipo.concepto,
|
||||
categoria: 'anticipo',
|
||||
precio: anticipo.monto || 0,
|
||||
status: anticipo.aplicado ? 'cancelled' : 'active'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Ordenar tabla
|
||||
@@ -1947,7 +2022,7 @@ function sortTable(field) {
|
||||
icon.textContent = '↕';
|
||||
});
|
||||
|
||||
const currentIcon = document.querySelector(`th[onclick="sortTable('${field}')"] .sort-icon`);
|
||||
const currentIcon = document.querySelector(`th[data-field="${field}"] .sort-icon`);
|
||||
if (currentIcon) {
|
||||
currentIcon.textContent = currentSortDirection === 'asc' ? '↑' : '↓';
|
||||
}
|
||||
@@ -1955,19 +2030,51 @@ function sortTable(field) {
|
||||
renderUnifiedProductsTable();
|
||||
}
|
||||
|
||||
// Editar producto unificado
|
||||
function editUnifiedProduct(id) {
|
||||
if (id.startsWith('anticipo-')) {
|
||||
// Manejar edición de anticipo
|
||||
const anticipoId = id.replace('anticipo-', '');
|
||||
const anticipo = movements.find(m => m.id == anticipoId);
|
||||
if (anticipo) {
|
||||
alert('La edición de anticipos se realiza desde el historial de ventas.');
|
||||
}
|
||||
return;
|
||||
// Inicializar filter modal
|
||||
function initializeFilterModal() {
|
||||
const filterToggleBtn = document.getElementById('filter-toggle-btn');
|
||||
const filterModal = document.getElementById('filter-modal');
|
||||
const filterModalClose = document.getElementById('filter-modal-close');
|
||||
|
||||
if (filterToggleBtn && filterModal) {
|
||||
filterToggleBtn.addEventListener('click', () => {
|
||||
filterModal.style.display = 'flex';
|
||||
});
|
||||
}
|
||||
|
||||
// Manejar edición de producto regular
|
||||
if (filterModalClose && filterModal) {
|
||||
filterModalClose.addEventListener('click', () => {
|
||||
filterModal.style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Cerrar modal al hacer click fuera de él
|
||||
if (filterModal) {
|
||||
filterModal.addEventListener('click', (e) => {
|
||||
if (e.target === filterModal) {
|
||||
filterModal.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Aplicar filtros desde el modal
|
||||
const filterInputs = filterModal?.querySelectorAll('input, select');
|
||||
if (filterInputs) {
|
||||
filterInputs.forEach(input => {
|
||||
input.addEventListener('change', applyFiltersFromModal);
|
||||
input.addEventListener('input', applyFiltersFromModal);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Aplicar filtros desde el modal
|
||||
function applyFiltersFromModal() {
|
||||
renderUnifiedProductsTable();
|
||||
}
|
||||
|
||||
// Editar producto unificado
|
||||
function editUnifiedProduct(id) {
|
||||
// Solo editar productos reales (servicios y cursos)
|
||||
const product = products.find(p => p.id == id);
|
||||
if (product) {
|
||||
document.getElementById('p-id').value = product.id;
|
||||
@@ -1975,18 +2082,12 @@ function editUnifiedProduct(id) {
|
||||
document.getElementById('p-type').value = product.type;
|
||||
document.getElementById('p-price').value = product.price || '';
|
||||
|
||||
// Mostrar/ocultar campos de anticipo
|
||||
toggleAnticipoFields(product.type);
|
||||
// Ya no hay campos de anticipo que mostrar
|
||||
}
|
||||
}
|
||||
|
||||
// Cambiar estado del producto
|
||||
async function toggleProductStatus(id) {
|
||||
if (id.startsWith('anticipo-')) {
|
||||
alert('El estado de los anticipos se maneja desde el sistema de ventas.');
|
||||
return;
|
||||
}
|
||||
|
||||
const product = products.find(p => p.id == id);
|
||||
if (!product) return;
|
||||
|
||||
@@ -2017,11 +2118,6 @@ async function toggleProductStatus(id) {
|
||||
|
||||
// Eliminar producto unificado
|
||||
async function deleteUnifiedProduct(id) {
|
||||
if (id.startsWith('anticipo-')) {
|
||||
alert('Los anticipos no se pueden eliminar desde aquí. Usa el historial de ventas.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('¿Estás seguro de que quieres eliminar este producto permanentemente?')) {
|
||||
try {
|
||||
const response = await fetch(`/api/products/${id}`, { method: 'DELETE' });
|
||||
@@ -2037,13 +2133,7 @@ async function deleteUnifiedProduct(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// Mostrar/ocultar campos de anticipo
|
||||
function toggleAnticipoFields(type) {
|
||||
const anticipoFields = document.getElementById('anticipo-fields');
|
||||
if (anticipoFields) {
|
||||
anticipoFields.style.display = type === 'anticipo' ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
// Los campos de anticipo fueron removidos - ya no son productos
|
||||
|
||||
// Función para actualizar tabla unificada después de cambios
|
||||
function updateUnifiedProductsAfterChange() {
|
||||
@@ -2053,24 +2143,23 @@ function updateUnifiedProductsAfterChange() {
|
||||
|
||||
// Inicializar controles de la tabla unificada
|
||||
function initializeUnifiedTable() {
|
||||
// Inicializar filter modal
|
||||
initializeFilterModal();
|
||||
|
||||
// Inicializar sorting para columnas
|
||||
document.querySelectorAll('.sortable').forEach(header => {
|
||||
header.addEventListener('click', () => {
|
||||
const field = header.getAttribute('data-field');
|
||||
if (field) {
|
||||
sortTable(field);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Verificar que los elementos existan antes de agregar listeners
|
||||
const filterType = document.getElementById('filter-type');
|
||||
const searchProducts = document.getElementById('search-products');
|
||||
const productTypeSelect = document.getElementById('p-type');
|
||||
|
||||
if (filterType) {
|
||||
filterType.addEventListener('change', renderUnifiedProductsTable);
|
||||
}
|
||||
|
||||
if (searchProducts) {
|
||||
searchProducts.addEventListener('input', renderUnifiedProductsTable);
|
||||
}
|
||||
|
||||
if (productTypeSelect) {
|
||||
productTypeSelect.addEventListener('change', (e) => {
|
||||
toggleAnticipoFields(e.target.value);
|
||||
});
|
||||
}
|
||||
// Ya no se necesita manejar campos de anticipo en productos
|
||||
|
||||
// Solo cargar si hay datos disponibles
|
||||
if (typeof products !== 'undefined' && typeof movements !== 'undefined') {
|
||||
@@ -2081,8 +2170,76 @@ function initializeUnifiedTable() {
|
||||
|
||||
// Exponer funciones globalmente para uso en onclick
|
||||
window.sortTable = sortTable;
|
||||
window.initializeFilterModal = initializeFilterModal;
|
||||
window.editUnifiedProduct = editUnifiedProduct;
|
||||
window.toggleProductStatus = toggleProductStatus;
|
||||
window.deleteUnifiedProduct = deleteUnifiedProduct;
|
||||
|
||||
function handleAnticipoSelection() {
|
||||
// 1. Primero pedir el monto del anticipo
|
||||
let anticipoAmount = prompt('💰 ANTICIPO\n\nIngresa el monto del anticipo:', '');
|
||||
if (anticipoAmount === null) return; // Usuario canceló
|
||||
|
||||
anticipoAmount = parseFloat(anticipoAmount);
|
||||
if (isNaN(anticipoAmount) || anticipoAmount <= 0) {
|
||||
alert('⚠️ Por favor ingresa un monto válido para el anticipo');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Preguntar tipo con confirm para hacer más fácil la selección
|
||||
const esServicio = confirm('🎯 TIPO DE ANTICIPO\n\n¿Es para un SERVICIO?\n\n✅ Aceptar = Servicio\n❌ Cancelar = Curso');
|
||||
|
||||
const productType = esServicio ? 'service' : 'course';
|
||||
const tipoTexto = esServicio ? 'servicio' : 'curso';
|
||||
|
||||
// 3. Obtener productos según el tipo
|
||||
const availableProducts = products.filter(p => p.type === productType);
|
||||
|
||||
if (availableProducts.length === 0) {
|
||||
alert(`❌ No hay ${tipoTexto}s disponibles`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Crear lista numerada para selección (solo nombres, sin precios)
|
||||
let productOptions = `🛍️ SELECCIONAR ${tipoTexto.toUpperCase()}\n\n`;
|
||||
availableProducts.forEach((product, index) => {
|
||||
productOptions += `${index + 1}. ${product.name}\n`;
|
||||
});
|
||||
|
||||
const selectedIndex = prompt(productOptions + '\n📝 Escribe el número de tu elección:');
|
||||
if (selectedIndex === null) return; // Usuario canceló
|
||||
|
||||
const productIndex = parseInt(selectedIndex) - 1;
|
||||
|
||||
if (isNaN(productIndex) || productIndex < 0 || productIndex >= availableProducts.length) {
|
||||
alert('❌ Selección inválida. Intenta de nuevo.');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedProduct = availableProducts[productIndex];
|
||||
const typeLabel = productType === 'course' ? 'Curso' : 'Servicio';
|
||||
|
||||
// 5. Crear nombre del anticipo para el ticket (tipo completo en paréntesis)
|
||||
const anticipoName = `Anticipo ${selectedProduct.name} $${parseFloat(anticipoAmount).toFixed(2)} (${typeLabel})`;
|
||||
|
||||
// 6. Agregar a productos seleccionados
|
||||
selectedProducts.push({
|
||||
id: 'anticipo-' + Date.now(),
|
||||
name: anticipoName,
|
||||
price: anticipoAmount,
|
||||
quantity: 1, // Los anticipos siempre son cantidad 1
|
||||
type: 'anticipo',
|
||||
productName: selectedProduct.name,
|
||||
productType: productType
|
||||
// No incluir originalPrice para evitar confusión en dashboard
|
||||
});
|
||||
|
||||
renderSelectedProducts();
|
||||
calculateTotals();
|
||||
showDynamicSections();
|
||||
|
||||
// Mensaje de confirmación
|
||||
alert(`✅ ANTICIPO AGREGADO\n\n${anticipoName}`);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||
|
||||
Reference in New Issue
Block a user