/**
* 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('
');
// CLIENTE PRIMERO
if (mov.client) lines.push(`
Cliente: ${esc(mov.client.nombre)}
`);
// DATOS DE CITA PROMINENTES
if (mov.fechaCita || mov.horaCita) {
lines.push('
');
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)}
`);
// Usar la función de fecha específica para tickets
const fechaFinal = generarFechaTicketFINAL();
console.log("FECHA GENERADA PARA TICKET:", fechaFinal);
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.staff) lines.push(`
Te atendió: ${esc(mov.staff)}
`);
if (mov.notas) lines.push(`
Notas: ${esc(mov.notas)}
`);
lines.push('
');
// TOTAL ALINEADO A LA DERECHA
lines.push(`
Total: $${montoFormateado}
`);
// MÉTODO DE PAGO DEBAJO DEL TOTAL - ALINEADO A LA DERECHA
if (mov.metodo) lines.push(`
Método: ${esc(mov.metodo)}
`);
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-05T20:43:00 - NUEVA FUNCIÓN generarFechaTicketFINAL