/** * Escapa caracteres HTML para prevenir XSS. * @param {string} str El string a escapar. * @returns {string} El string escapado. */ function esc(str) { return String(str || '').replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': '"', "'": "'" }[c])); } /** * Genera el HTML para un ticket de movimiento. * @param {object} mov El objeto del movimiento. * @param {object} settings El objeto de configuración. * @returns {string} El HTML del ticket. */ function templateTicket(mov, settings) { // FUNCIÓN DE FECHA DEFINITIVA - NO MAS UNDEFINED NUNCA MAS function generarFechaTicketFINAL() { // Crear nueva fecha DIRECTAMENTE const fecha = new Date(); // Obtener componentes de fecha SIN VARIABLES INTERMEDIAS const dia = fecha.getDate().toString().padStart(2, '0'); const mes = (fecha.getMonth() + 1).toString().padStart(2, '0'); const año = fecha.getFullYear().toString(); const hora = fecha.getHours().toString().padStart(2, '0'); const minutos = fecha.getMinutes().toString().padStart(2, '0'); // Construir fecha final DIRECTAMENTE const resultado = `${dia}/${mes}/${año} ${hora}:${minutos}`; console.log("FECHA FINAL DEFINITIVA GENERADA:", resultado); return resultado; } const montoFormateado = Number(mov.monto).toFixed(2); const tipoServicio = mov.subtipo === 'Retoque' ? `Retoque de ${mov.tipo}` : mov.tipo; const lines = []; lines.push('
'); lines.push(''); // Información del negocio - verificar estructura de settings // Extraer datos desde settings o settings.settings (doble anidación) const businessData = settings?.settings || settings || {}; const negocioNombre = businessData?.negocio || settings?.negocio || 'Ale Ponce'; const negocioTagline = businessData?.tagline || settings?.tagline || 'beauty expert'; const negocioCalle = businessData?.calle || settings?.calle; const negocioColonia = businessData?.colonia || settings?.colonia; const negocioCP = businessData?.cp || settings?.cp; const negocioRFC = businessData?.rfc || settings?.rfc; const negocioTel = businessData?.tel || settings?.tel || '8443555108'; lines.push(`
${esc(negocioNombre)}
`); lines.push(`
${esc(negocioTagline)}
`); lines.push('
'); if (negocioCalle) lines.push(`
${esc(negocioCalle)}
`); if (negocioColonia && negocioCP) lines.push(`
${esc(negocioColonia)}, ${esc(negocioCP)}
`); if (negocioRFC) lines.push(`
RFC: ${esc(negocioRFC)}
`); lines.push(`
Tel: ${esc(negocioTel)}
`); lines.push('
'); // INFORMACIÓN COMPACTA DEL CLIENTE Y CITA if (mov.client) lines.push(`
Cliente: ${esc(mov.client.nombre)}
`); lines.push('
'); // INFO EN LÍNEAS COMPACTAS if (mov.fechaCita && mov.horaCita) { lines.push(`
Cita: ${esc(mov.fechaCita)} - ${esc(mov.horaCita)}
`); } else { if (mov.fechaCita) lines.push(`
Fecha de Cita: ${esc(mov.fechaCita)}
`); if (mov.horaCita) lines.push(`
Hora de Cita: ${esc(mov.horaCita)}
`); } lines.push('
'); lines.push(`
Folio: ${esc(mov.folio)}
`); // Fecha de Venta const fechaMovimiento = new Date(mov.fechaISO); 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(`
Fecha de Venta: ${esc(fechaFinal)}
`); lines.push('
'); lines.push(`
${esc(tipoServicio)}
`); // CONCEPTO CON PRECIO if (mov.concepto) { lines.push(`
Concepto:
`); lines.push(`
${esc(mov.concepto)}$${montoFormateado}
`); } // DESCUENTOS SI EXISTEN if (mov.descuento && mov.descuento > 0) { 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(`
${descriptionText}:${amountText}
`); // Mostrar comentario del descuento si existe if (discountInfo.reason && discountInfo.reason.trim()) { lines.push(`
Motivo: ${esc(discountInfo.reason)}
`); } } else { // Fallback para formato anterior lines.push(`
Descuento ${mov.motivoDescuento ? '(' + esc(mov.motivoDescuento) + ')' : ''}:-$${Number(mov.descuento).toFixed(2)}
`); } } if (mov.notas) lines.push(`
Notas: ${esc(mov.notas)}
`); lines.push('
'); // TOTAL ALINEADO A LA IZQUIERDA lines.push(`
Total: $${montoFormateado}
`); // MÉTODO DE PAGO DEBAJO DEL TOTAL - ALINEADO A LA IZQUIERDA if (mov.metodo) lines.push(`
Método: ${esc(mov.metodo)}
`); // TE ATENDIÓ DEBAJO DEL MÉTODO DE PAGO if (mov.staff) lines.push(`
Te atendió: ${esc(mov.staff)}
`); if (mov.client && (mov.client.esOncologico || mov.client.consentimiento)) { lines.push('
'); if (mov.client.esOncologico) { lines.push('
Consentimiento Oncológico
'); lines.push(`
El cliente declara ser paciente oncológico y que la información de su médico es veraz.
`); if (mov.client.nombreMedico) lines.push(`
Médico: ${esc(mov.client.nombreMedico)}
`); if (mov.client.telefonoMedico) lines.push(`
Tel. Médico: ${esc(mov.client.telefonoMedico)}
`); if (mov.client.cedulaMedico) lines.push(`
Cédula: ${esc(mov.client.cedulaMedico)}
`); } lines.push('
'); const consentText = mov.tipo === 'Curso' || tipoServicio.toLowerCase().includes('curso') ? 'Al inscribirse al curso, acepta los términos y condiciones del programa educativo.' : 'Al consentir el servicio, declara que la información médica proporcionada es veraz.'; lines.push(`
${consentText}
`); } lines.push('
'); lines.push('
¡Tu opinión es muy importante!
'); lines.push('
Escanea el código QR para darnos tu feedback.
'); lines.push(''); lines.push('
'); const negocioLeyenda = businessData?.leyenda || settings?.leyenda; if (negocioLeyenda) lines.push(``); lines.push('
'); return lines.join(''); } /** * Renderiza el ticket en el DOM, genera el QR y llama a la función de impresión. * @param {object} mov El objeto del movimiento. * @param {object} settings El objeto de configuración. */ export async function renderTicketAndPrint(mov, settings) { const printArea = document.getElementById('printArea'); if (!printArea) { console.error("El área de impresión #printArea no se encontró."); alert("Error: No se encontró el área de impresión. Contacte al soporte."); return; } try { printArea.innerHTML = templateTicket(mov, settings); const canvas = document.getElementById('qr-canvas'); if (!canvas) { console.error("El canvas del QR #qr-canvas no se encontró. Se imprimirá sin QR."); window.print(); return; } const qrUrl = 'http://vanityexperience.mx/qr'; await QRCode.toCanvas(canvas, qrUrl, { width: 140, margin: 1 }); requestAnimationFrame(() => window.print()); } catch (error) { console.error("Error al intentar imprimir:", error); alert(`Ocurrió un error al preparar la impresión: ${error.message}. Revise la consola para más detalles.`); } } document.addEventListener('DOMContentLoaded', () => { const btnTestTicket = document.getElementById('btnTestTicket'); if (btnTestTicket) { btnTestTicket.addEventListener('click', () => { const demoMovement = { id: 'demo', folio: 'DEMO-000001', fechaISO: new Date().toISOString(), client: { nombre: 'Cliente de Prueba', esOncologico: true, nombreMedico: 'Dr. Juan Pérez', telefonoMedico: '5512345678', cedulaMedico: '1234567' }, tipo: 'Pago', monto: 123.45, metodo: 'Efectivo', concepto: 'Producto de demostración', staff: 'Admin', notas: 'Esta es una impresión de prueba.' }; renderTicketAndPrint(demoMovement, window.settings || {}); }); } }); // FORZAR RECARGA - 2025-09-09T21:33:00 - TODO ALINEADO A LA IZQUIERDA