diff --git a/PNG_RECEIPT_PROGRESS.md b/PNG_RECEIPT_PROGRESS.md new file mode 100644 index 0000000..c641438 --- /dev/null +++ b/PNG_RECEIPT_PROGRESS.md @@ -0,0 +1,140 @@ +# 📋 PNG Receipt System - Progress Tracker + +## 🎯 Estado del Proyecto +**Iniciado**: 2025-09-14 +**Estado Actual**: 📝 Planificación Completa +**Próximo**: 🚀 Fase 1 - Setup + +--- + +## ✅ Tasks Completadas + +### 📝 Planificación y Documentación +- [x] **Análisis del sistema actual** - Comprensión completa de `print.js` y generación de tickets +- [x] **Identificación de casos de uso** - Matriz completa de combinaciones de tickets +- [x] **Especificación técnica** - HTML/CSS structure y assets strategy +- [x] **Plan de desarrollo** - 6 fases con estimaciones detalladas +- [x] **Documentación README** - Especificación completa en `PNG_RECEIPT_README.md` +- [x] **Setup de tracking** - Este documento de progreso + +**Tiempo Total Planificación**: ~1 hora + +--- + +## 🔄 Tasks en Progreso + +*Ninguna actualmente - Listo para comenzar desarrollo* + +--- + +## 📋 Tasks Pendientes + +### **FASE 1: Setup con Assets (45 min)** +- [ ] Crear directorio `/assets/receipt/` y subir assets +- [ ] Agregar librerías CDN: `html2canvas` + `FileSaver.js` +- [ ] Crear archivo `receipt.js` +- [ ] Crear archivo `receipt.css` con estilos base +- [ ] Crear HTML structure base en memoria +- [ ] Test de carga de assets y CSS + +### **FASE 2: Template Engine + Lógica (60 min)** +- [ ] Implementar `analyzeTicketType()` function +- [ ] Crear templates dinámicos por caso +- [ ] Sistema de mapeo: `movement` → `receiptData` +- [ ] Función `generateReceiptHTML()` +- [ ] Test básico de renderizado + +### **FASE 3: Contenido Dinámico y Médico (45 min)** +- [ ] Implementar sección consentimiento médico +- [ ] Sistema de datos oncológicos (médico, tel, cédula) +- [ ] Formato de información de citas +- [ ] Función `getConsentText()` +- [ ] Test con datos médicos completos + +### **FASE 4: Notas Anticipo Inteligentes (30 min)** +- [ ] Lógica `shouldShowAnticipoNotes()` +- [ ] Templates condicionales para anticipos +- [ ] Diferenciación: anticipo puro vs aplicado +- [ ] Test matrix casos anticipo + +### **FASE 5: Generación PNG (30 min)** +- [ ] Configuración `html2canvas` optimizada +- [ ] Sistema de nombres: `Ticket_{FOLIO}.png` +- [ ] Función `downloadReceiptPNG()` principal +- [ ] Integration con tabla de tickets +- [ ] Test de descarga y calidad + +### **FASE 6: Testing Final (45 min)** +- [ ] Test exhaustivo con todos los casos +- [ ] Verificación calidad PNG en diferentes devices +- [ ] Test compatibilidad navegadores +- [ ] Performance testing +- [ ] Documentación final de uso + +--- + +## 🧪 Test Matrix Status + +| Caso de Uso | Planificado | Implementado | Testeado | +|-------------|-------------|--------------|----------| +| Servicio Simple | ✅ | ❌ | ❌ | +| Servicio + Cita | ✅ | ❌ | ❌ | +| Anticipo Puro | ✅ | ❌ | ❌ | +| Servicio + Anticipo Aplicado | ✅ | ❌ | ❌ | +| Cliente Consentimiento | ✅ | ❌ | ❌ | +| Paciente Oncológico | ✅ | ❌ | ❌ | +| Combo Completo | ✅ | ❌ | ❌ | + +--- + +## 📊 Estimaciones vs Tiempo Real + +| Fase | Estimado | Real | Diferencia | Estado | +|------|----------|------|------------|---------| +| Planificación | 60min | 60min | ✅ 0min | ✅ Completa | +| Fase 1: Setup | 45min | -min | - | 📋 Pendiente | +| Fase 2: Templates | 60min | -min | - | 📋 Pendiente | +| Fase 3: Contenido Médico | 45min | -min | - | 📋 Pendiente | +| Fase 4: Anticipo Logic | 30min | -min | - | 📋 Pendiente | +| Fase 5: PNG Generation | 30min | -min | - | 📋 Pendiente | +| Fase 6: Testing | 45min | -min | - | 📋 Pendiente | +| **TOTAL** | **4h 15min** | **1h** | - | 🔄 **En progreso** | + +--- + +## 🚨 Issues y Blockers + +*Ninguno identificado actualmente* + +--- + +## 📝 Notas de Desarrollo + +### Assets Preparados +- ✅ Background pattern +- ✅ Logo corporativo +- ✅ Business name imagen +- ✅ Tagline imagen +- ✅ "Comprobante" título +- ✅ Rectángulo blanco container + +### Decisiones Técnicas +- **Font**: Montserrat para todo el contenido dinámico +- **Width**: 400px optimizado para móvil +- **Format**: PNG con transparencia +- **Quality**: Scale 2x para HD +- **Integration**: Botón separado en tabla tickets + +--- + +## 🔄 Updates Log + +**2025-09-14** +- ✅ Proyecto iniciado y planificado completamente +- ✅ README técnico creado +- ✅ Sistema de tracking establecido +- 🎯 Listo para Fase 1 cuando assets estén disponibles + +--- + +**Próximo Update**: Después de completar Fase 1 \ No newline at end of file diff --git a/PNG_RECEIPT_README.md b/PNG_RECEIPT_README.md new file mode 100644 index 0000000..8b512b6 --- /dev/null +++ b/PNG_RECEIPT_README.md @@ -0,0 +1,312 @@ +# 📱 Sistema de Recibos PNG - Especificación Completa + +## 🎯 Objetivo +Crear un sistema paralelo que genere recibos PNG elegantes para enviar a clientes, manteniendo el sistema de impresión térmica actual intacto. + +**Archivo de descarga**: `Ticket_{FOLIO}.png` (ej: `Ticket_AP-k8hcg.png`) + +--- + +## 🎨 Assets Structure +``` +/assets/receipt/ +├── background.png (fondo decorativo completo) +├── logo.png (logotipo del negocio) +├── business-name.png (nombre del negocio) +├── tagline.png (tagline: "Beauty Expert", etc.) +├── comprobante-title.png (título "COMPROBANTE DE PAGO") +└── rectangle-white.png (rectángulo contenedor blanco/transparente) +``` + +## 🏗️ Arquitectura del Sistema + +### HTML Structure +```html +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+ +
+
+ +
+
+
+``` + +### CSS Base +```css +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap'); + +.receipt-wrapper { + width: 400px; + font-family: 'Montserrat', sans-serif; +} + +.receipt-background { + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +.receipt-content { + background-size: contain; + background-position: center; + background-repeat: no-repeat; +} + +.montserrat { font-family: 'Montserrat', sans-serif; } +.bold { font-weight: 600; } +.extra-bold { font-weight: 700; } +``` + +--- + +## 🎫 Tipos de Tickets y Casos de Uso + +### Matriz de Combinaciones +| Tipo | Cita | Anticipo | Consentimiento | Oncológico | Notas Especiales | +|------|------|----------|----------------|------------|------------------| +| **Servicio Simple** | ❌ | ❌ | ❌ | ❌ | - | +| **Servicio con Cita** | ✅ | ❌ | ❌ | ❌ | Fecha/Hora | +| **Anticipo Puro** | ✅ | ✅ | ❌ | ❌ | Notas anticipo | +| **Servicio + Anticipo Aplicado** | ✅ | ✅ | ❌ | ❌ | **SIN** notas anticipo | +| **Servicio + Consentimiento** | ✅ | ❌ | ✅ | ❌ | Texto consentimiento | +| **Servicio + Oncológico** | ✅ | ❌ | ✅ | ✅ | Datos médico | +| **Combo Completo** | ✅ | ✅ | ✅ | ✅ | Todo combinado | + +### Lógica de Detección +```javascript +function analyzeTicketType(movement) { + return { + hasAppointment: !!(movement.fechaCita && movement.horaCita), + isAnticipo: movement.tipo === 'Anticipo' || + movement.concepto?.toLowerCase().includes('anticipo'), + hasAnticipoApplied: movement.discountInfo?.type === 'anticipo', + hasConsent: movement.client?.consentimiento || movement.client?.esOncologico, + isOncology: movement.client?.esOncologico, + needsAnticipoNotes: function() { + // Solo mostrar notas si es anticipo PURO (no aplicado a servicio) + return this.isAnticipo && !this.hasAnticipoApplied; + } + }; +} +``` + +### Casos Específicos + +#### 1. Anticipo Puro +- **Mostrar**: Notas de compromiso y cancelación +- **Condición**: `movement.tipo === 'Anticipo'` Y NO tiene `discountInfo.type === 'anticipo'` + +#### 2. Servicio + Anticipo Aplicado +- **NO mostrar**: Notas de anticipo +- **Condición**: Servicio normal con descuento de anticipo aplicado + +#### 3. Paciente Oncológico +- **Mostrar**: Datos del médico (nombre, teléfono, cédula) +- **Condición**: `movement.client.esOncologico === true` + +#### 4. Consentimiento Médico +- **Mostrar**: Texto de consentimiento específico +- **Condición**: `movement.client.consentimiento === true` + +--- + +## 🔄 Plan de Desarrollo + +### **FASE 1: Setup con Assets (45 min)** +- [ ] Agregar librerías: `html2canvas` + `FileSaver.js` +- [ ] Crear archivo `receipt.js` para lógica PNG +- [ ] Crear archivo `receipt.css` para estilos móviles +- [ ] Setup HTML base con assets structure +- [ ] Test de carga de assets + +### **FASE 2: Template Engine + Lógica de Casos (60 min)** +- [ ] Sistema de detección de tipos de ticket +- [ ] Templates dinámicos por caso de uso +- [ ] Mapeo de datos del movimiento a template +- [ ] Test de renderizado por tipo + +### **FASE 3: Contenido Dinámico y Datos Médicos (45 min)** +- [ ] Sección de consentimiento médico +- [ ] Datos de pacientes oncológicos +- [ ] Información de citas (fecha/hora) +- [ ] Test con datos médicos reales + +### **FASE 4: Notas de Anticipo Inteligentes (30 min)** +- [ ] Lógica para mostrar/ocultar notas de anticipo +- [ ] Templates condicionales +- [ ] Test de casos anticipo vs servicio+anticipo + +### **FASE 5: Generación PNG y Descarga (30 min)** +- [ ] Configuración `html2canvas` optimizada para móvil +- [ ] Sistema de nombres: `Ticket_{FOLIO}.png` +- [ ] Función de descarga automática +- [ ] Test de calidad de imagen + +### **FASE 6: Testing Exhaustivo (45 min)** +- [ ] Test matrix con todos los casos +- [ ] Verificación de calidad PNG +- [ ] Compatibilidad navegadores +- [ ] Test de rendimiento + +--- + +## 🧪 Test Scenarios + +```javascript +const testScenarios = [ + // Básicos + { + desc: "Servicio simple", + data: { tipo: "service", client: null, fechaCita: null }, + expect: { noAnticipoNotes: true, noMedicalData: true } + }, + + { + desc: "Servicio con cita", + data: { tipo: "service", client: "normal", fechaCita: "2025-01-15", horaCita: "10:00" }, + expect: { hasAppointment: true } + }, + + // Anticipos + { + desc: "Anticipo puro", + data: { tipo: "Anticipo" }, + expect: { hasAnticipoNotes: true } + }, + + { + desc: "Servicio + anticipo aplicado", + data: { tipo: "service", discountInfo: { type: "anticipo" }}, + expect: { hasAnticipoNotes: false } + }, + + // Médicos + { + desc: "Paciente oncológico completo", + data: { + client: { + esOncologico: true, + nombreMedico: "Dr. Juan", + telefonoMedico: "123456", + cedulaMedico: "ABC123" + } + }, + expect: { hasMedicalData: true, hasConsentText: true } + } +]; +``` + +--- + +## 📦 Dependencias Requeridas + +### Librerías JavaScript +```html + + + +``` + +### Google Fonts +```css +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap'); +``` + +--- + +## 🚀 Integración con Sistema Actual + +### Botón en Tabla de Tickets +```html + + + + +``` + +### Función Principal +```javascript +async function downloadReceiptPNG(movementId) { + try { + // 1. Obtener datos del movimiento + const movement = await getMovementById(movementId); + + // 2. Analizar tipo de ticket + const ticketType = analyzeTicketType(movement); + + // 3. Generar HTML del recibo + const receiptHTML = generateReceiptHTML(movement, ticketType); + + // 4. Convertir a PNG + const canvas = await html2canvas(receiptHTML, pngConfig); + + // 5. Descargar + canvas.toBlob(blob => { + saveAs(blob, `Ticket_${movement.folio}.png`); + }); + + } catch (error) { + console.error('Error generating receipt:', error); + alert('Error al generar el recibo'); + } +} +``` + +--- + +## 📏 Configuración PNG + +```javascript +const pngConfig = { + scale: 2, // HD quality + width: 400, // Mobile optimal width + height: 'auto', + backgroundColor: 'transparent', // Usar fondo del asset + useCORS: true, // Para assets externos + allowTaint: true, + ignoreElements: (element) => { + // Ignorar elementos que no queremos en el PNG + return element.classList?.contains('no-png'); + } +}; +``` + +--- + +## 🎯 Resultado Final + +**✅ Sistema que genera recibos PNG:** +- Diseño profesional idéntico al mockup +- Maneja TODOS los casos del negocio +- Descarga automática con nombre correcto +- No interfiere con sistema de impresión actual +- Optimizado para móvil y WhatsApp + +**📱 Flujo de Usuario:** +1. Cliente termina servicio → Se imprime ticket térmico (como siempre) +2. Si cliente quiere recibo digital → Staff hace clic en "📱 Enviar Recibo" +3. Se descarga PNG → Staff envía por WhatsApp/email + +**Estimación Total: 4 horas de desarrollo + testing** \ No newline at end of file diff --git a/app.js b/app.js index 3824f48..28fadf0 100644 --- a/app.js +++ b/app.js @@ -1025,23 +1025,35 @@ function renderTable() { tr.insertCell().textContent = Number(mov.monto).toFixed(2); const actionsCell = tr.insertCell(); - + + // Botón de descarga PNG + const pngButton = document.createElement('button'); + pngButton.className = 'action-btn btn-success'; + pngButton.dataset.id = mov.id; + pngButton.dataset.action = 'download-png'; + pngButton.innerHTML = 'payment'; + pngButton.title = 'Descargar recibo PNG para compartir'; + pngButton.style.marginRight = '5px'; + actionsCell.appendChild(pngButton); + // Botón de solicitar cancelación para todos los usuarios const cancelRequestButton = document.createElement('button'); cancelRequestButton.className = 'action-btn btn-warning'; cancelRequestButton.dataset.id = mov.id; cancelRequestButton.dataset.action = 'request-cancel'; - cancelRequestButton.textContent = 'Solicitar Cancelación'; + cancelRequestButton.innerHTML = 'cancel'; + cancelRequestButton.title = 'Solicitar Cancelación'; cancelRequestButton.style.marginRight = '5px'; actionsCell.appendChild(cancelRequestButton); - + // Solo mostrar botón de eliminar para administradores if (currentUser && currentUser.role === 'admin') { const deleteButton = document.createElement('button'); deleteButton.className = 'action-btn btn-danger'; deleteButton.dataset.id = mov.id; deleteButton.dataset.action = 'delete'; - deleteButton.textContent = 'Eliminar'; + deleteButton.innerHTML = 'delete'; + deleteButton.title = 'Eliminar permanentemente'; actionsCell.appendChild(deleteButton); } }); @@ -1837,7 +1849,7 @@ function handleTableClick(e) { const id = actionBtn.dataset.id; const action = actionBtn.dataset.action; - if (action === 'reprint' || action === 'delete' || action === 'request-cancel') { + if (action === 'reprint' || action === 'delete' || action === 'request-cancel' || action === 'download-png') { const movement = movements.find(m => m.id === id); if (movement) { if (action === 'reprint') { @@ -1847,6 +1859,8 @@ function handleTableClick(e) { deleteMovement(id); } else if (action === 'request-cancel') { showCancellationRequestModal(id, movement); + } else if (action === 'download-png') { + downloadPNGReceipt(id, movement); } } } else if (action === 'edit-user') { @@ -3256,6 +3270,44 @@ function setCorrectClearShortcut() { } } +// --- PNG AND PDF DOWNLOAD FUNCTIONS --- +function downloadPNGReceipt(movementId, movement) { + try { + console.log('Downloading PNG receipt for movement:', movementId); + console.log('Movement data:', movement); + + // Prepare movement data with client info + const client = clients.find(c => c.id === movement.clienteId); + console.log('Found client:', client); + + const movementWithClient = { + ...movement, + client: client || null, + cliente: client ? client.nombre : 'Cliente General', + telefonoCliente: client ? client.telefono : null + }; + + console.log('Movement with client:', movementWithClient); + + // Check if required libraries are loaded + console.log('html2canvas available:', typeof html2canvas !== 'undefined'); + console.log('saveAs available:', typeof saveAs !== 'undefined'); + console.log('pngReceiptGenerator available:', typeof window.pngReceiptGenerator !== 'undefined'); + + // Use the PNG receipt generator + if (typeof window.pngReceiptGenerator !== 'undefined' && window.pngReceiptGenerator) { + console.log('Calling pngReceiptGenerator.downloadReceiptPNG...'); + window.pngReceiptGenerator.downloadReceiptPNG(movementId, movementWithClient); + } else { + throw new Error('Sistema PNG no disponible - Verificar que receipt.js esté cargado'); + } + } catch (error) { + console.error('Error downloading PNG receipt:', error); + alert('Error al generar el recibo PNG: ' + error.message); + } +} + + document.addEventListener('DOMContentLoaded', () => { initializeApp(); setCorrectClearShortcut(); diff --git a/assets/receipt/background.png b/assets/receipt/background.png new file mode 100644 index 0000000..35cc32f Binary files /dev/null and b/assets/receipt/background.png differ diff --git a/assets/receipt/comprobante-title.svg b/assets/receipt/comprobante-title.svg new file mode 100644 index 0000000..1b21f7f --- /dev/null +++ b/assets/receipt/comprobante-title.svg @@ -0,0 +1,171 @@ + + diff --git a/assets/receipt/isotipo.svg b/assets/receipt/isotipo.svg new file mode 100644 index 0000000..0033bdf --- /dev/null +++ b/assets/receipt/isotipo.svg @@ -0,0 +1,145 @@ + + diff --git a/assets/receipt/logo.svg b/assets/receipt/logo.svg new file mode 100644 index 0000000..06b7967 --- /dev/null +++ b/assets/receipt/logo.svg @@ -0,0 +1,387 @@ + + diff --git a/index.html b/index.html index eeb9db0..caec25d 100644 --- a/index.html +++ b/index.html @@ -15,8 +15,10 @@ + + @@ -775,6 +777,10 @@
+ + + + \ No newline at end of file diff --git a/print.js b/print.js index 54d90f8..588c477 100644 --- a/print.js +++ b/print.js @@ -151,7 +151,7 @@ function templateTicket(mov, settings) { lines.push('
'); lines.push('
Al dejar tu anticipo, te agradecemos tu compromiso con nuestro tiempo, de la misma forma en que nosotros respetamos el tuyo.
'); lines.push('
'); - lines.push('
Las cancelaciones con menos de 24 horas no son reembolsables.
'); + lines.push('
Las cancelaciones con menos de 48 horas no son reembolsables.
'); lines.push('
'); } @@ -205,7 +205,7 @@ function templateTicket(mov, settings) { * @param {object} mov El objeto del movimiento. * @param {object} settings El objeto de configuración. */ -export async function renderTicketAndPrint(mov, settings) { +export async function renderTicketAndPrint(mov, settings, options = {}) { const printArea = document.getElementById('printArea'); if (!printArea) { console.error("El área de impresión #printArea no se encontró."); @@ -219,14 +219,23 @@ export async function renderTicketAndPrint(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(); + if (options.saveAsPDF) { + await generatePDF(mov); + } else { + window.print(); + } return; } const qrUrl = 'http://vanityexperience.mx/qr'; await QRCode.toCanvas(canvas, qrUrl, { width: 140, margin: 1 }); - requestAnimationFrame(() => window.print()); + if (options.saveAsPDF) { + // Generate PDF without printing + await generatePDF(mov); + } else { + requestAnimationFrame(() => window.print()); + } } catch (error) { console.error("Error al intentar imprimir:", error); @@ -262,4 +271,78 @@ document.addEventListener('DOMContentLoaded', () => { } }); +// PDF Generation function +async function generatePDF(mov) { + try { + console.log('Generating PDF for movement:', mov.folio); + + // Get the print area that contains the rendered ticket + const printArea = document.getElementById('printArea'); + if (!printArea) { + throw new Error('Print area not found'); + } + + // Create a temporary container for PDF generation + const tempContainer = document.createElement('div'); + tempContainer.innerHTML = printArea.innerHTML; + tempContainer.style.position = 'absolute'; + tempContainer.style.left = '-9999px'; + tempContainer.style.top = '-9999px'; + tempContainer.style.width = '58mm'; + tempContainer.style.background = 'white'; + document.body.appendChild(tempContainer); + + // Use html2canvas to convert to image, then create PDF + const canvas = await html2canvas(tempContainer, { + scale: 2, + width: 220, // ~58mm in pixels + height: 'auto', + backgroundColor: 'white', + useCORS: true, + allowTaint: true + }); + + console.log('PDF Canvas created, dimensions:', canvas.width, 'x', canvas.height); + + // Clean up temporary container + document.body.removeChild(tempContainer); + + // Validate canvas before creating blob + if (canvas.width === 0 || canvas.height === 0) { + throw new Error('Canvas has invalid dimensions: ' + canvas.width + 'x' + canvas.height); + } + + // Convert canvas to blob and download + canvas.toBlob(blob => { + if (!blob) { + console.error('Failed to create blob from canvas'); + alert('Error al generar el PDF del ticket - blob creation failed'); + return; + } + + console.log('Blob created successfully:', blob.size, 'bytes'); + + try { + // Create a link and trigger download + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `Ticket_${mov.folio}_thermal.png`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(link.href); + + console.log('PDF (PNG) ticket downloaded successfully'); + } catch (urlError) { + console.error('Error creating object URL:', urlError); + alert('Error al generar el enlace de descarga: ' + urlError.message); + } + }, 'image/png'); + + } catch (error) { + console.error('Error generating PDF:', error); + alert('Error al generar el PDF del ticket: ' + error.message); + } +} + // FORZAR RECARGA - 2025-09-09T21:33:00 - TODO ALINEADO A LA IZQUIERDA \ No newline at end of file diff --git a/receipt.css b/receipt.css new file mode 100644 index 0000000..3e25e78 --- /dev/null +++ b/receipt.css @@ -0,0 +1,315 @@ +/** + * PNG Receipt Styles - Based on broad_idea.png design + * Mobile-optimized receipt design using Montserrat font + * Matches the elegant layout shown in the reference image + */ + +@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap'); + +/* Base receipt container */ +.receipt-wrapper { + width: 400px; + font-family: 'Montserrat', sans-serif; + position: relative; + box-sizing: border-box; +} + +/* Background decorative layer */ +.receipt-background { + background-size: cover; + background-position: center; + background-repeat: no-repeat; + padding: 20px; + min-height: 600px; +} + +/* Main receipt container */ +.receipt-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 15px; +} + +/* Header section with logo and branding */ +.receipt-header { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-bottom: 10px; +} + +.logo-img { + max-width: 80px; + height: auto; +} + +.business-name-img { + max-width: 200px; + height: auto; +} + +.tagline-img { + max-width: 150px; + height: auto; +} + +/* Content section over white rectangle */ +.receipt-content { + background-size: contain; + background-position: center; + background-repeat: no-repeat; + padding: 30px 25px; + width: 100%; + max-width: 350px; + min-height: 400px; + display: flex; + flex-direction: column; + align-items: center; +} + +.title-img { + max-width: 250px; + height: auto; + margin-bottom: 20px; +} + +/* Dynamic content with Montserrat */ +.dynamic-content { + width: 100%; + color: #333; + line-height: 1.5; +} + +/* Typography classes */ +.montserrat { + font-family: 'Montserrat', sans-serif; +} + +.regular { + font-weight: 400; +} + +.bold { + font-weight: 600; +} + +.extra-bold { + font-weight: 700; +} + +/* Content sections */ +.folio-section, +.date-section, +.client-section { + margin-bottom: 8px; + font-size: 14px; +} + +.services-section { + margin: 20px 0; + width: 100%; +} + +.services-table { + width: 100%; + border-collapse: collapse; + margin: 10px 0; +} + +.services-table th, +.services-table td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; + font-size: 13px; +} + +.services-table th { + font-weight: 600; + background-color: rgba(0,0,0,0.05); +} + +.services-table .amount { + text-align: right; + font-weight: 600; +} + +/* Appointment section */ +.appointment-section { + background-color: rgba(0,0,0,0.03); + padding: 10px; + border-radius: 5px; + margin: 10px 0; + text-align: center; +} + +.appointment-section .date { + font-weight: 600; + font-size: 15px; +} + +.appointment-section .time { + font-size: 13px; + color: #666; +} + +/* Medical consent section */ +.medical-section { + background-color: rgba(255, 240, 240, 0.5); + padding: 15px; + border-left: 4px solid #ff6b6b; + margin: 15px 0; + border-radius: 3px; +} + +.medical-section h4 { + margin: 0 0 10px 0; + font-size: 14px; + font-weight: 700; + color: #d63384; +} + +.medical-section p { + margin: 5px 0; + font-size: 12px; + line-height: 1.4; +} + +.medical-data { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid rgba(214, 51, 132, 0.2); +} + +.medical-data p { + margin: 3px 0; + font-size: 11px; +} + +/* Anticipo notes section */ +.anticipo-notes { + background-color: rgba(255, 248, 220, 0.8); + padding: 12px; + border-left: 4px solid #ffc107; + margin: 15px 0; + border-radius: 3px; +} + +.anticipo-notes p { + margin: 8px 0; + font-size: 12px; + line-height: 1.4; +} + +.anticipo-notes .highlight { + font-weight: 600; + color: #b8860b; +} + +/* Payment summary section */ +.payment-summary { + margin-top: 20px; + padding-top: 15px; + border-top: 2px solid #333; + width: 100%; +} + +.total-amount { + font-size: 18px; + font-weight: 700; + text-align: center; + margin: 10px 0; +} + +.payment-method, +.staff-info { + text-align: center; + font-size: 13px; + margin: 5px 0; +} + +.payment-method .method, +.staff-info .name { + font-weight: 600; +} + +/* Footer section */ +.receipt-footer { + margin-top: 20px; + text-align: center; + padding: 15px; +} + +.contact-info { + display: flex; + justify-content: center; + gap: 20px; + margin: 10px 0; +} + +.contact-item { + display: flex; + align-items: center; + gap: 5px; + font-size: 12px; +} + +.contact-icon { + width: 16px; + height: 16px; +} + +.footer-message { + font-size: 14px; + font-weight: 600; + color: #333; + margin-top: 15px; + font-style: italic; +} + +/* Utility classes */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } + +.mb-small { margin-bottom: 5px; } +.mb-medium { margin-bottom: 10px; } +.mb-large { margin-bottom: 15px; } + +.mt-small { margin-top: 5px; } +.mt-medium { margin-top: 10px; } +.mt-large { margin-top: 15px; } + +/* Responsive adjustments */ +@media (max-width: 420px) { + .receipt-wrapper { + width: 100%; + max-width: 380px; + } + + .receipt-content { + padding: 20px 15px; + } + + .services-table th, + .services-table td { + padding: 6px 4px; + font-size: 12px; + } +} + +/* Print/PNG specific styles */ +@media print, .png-export { + .receipt-wrapper { + box-shadow: none; + border: none; + } + + /* Ensure high quality for PNG export */ + * { + -webkit-print-color-adjust: exact; + color-adjust: exact; + } +} \ No newline at end of file diff --git a/receipt.js b/receipt.js new file mode 100644 index 0000000..5b7eb9d --- /dev/null +++ b/receipt.js @@ -0,0 +1,881 @@ +// PNG Receipt System - Based on broad_idea.png design +// Phase 1: Setup with Assets + +class PNGReceiptGenerator { + constructor() { + this.pngConfig = { + scale: 2, + width: 540, // 1080/2 for mobile format + height: 960, // 1920/2 for mobile format + backgroundColor: 'white', + useCORS: true, + allowTaint: true, + ignoreElements: (element) => { + return element.classList?.contains('no-png'); + } + }; + } + + hasAnyDiscount(movement) { + // Check discountInfo.amount + if (movement.discountInfo && parseFloat(movement.discountInfo.amount || '0') > 0) { + console.log('🎯 Found discount in discountInfo.amount:', movement.discountInfo.amount); + return true; + } + + // Check for descuento field directly + if (movement.descuento && parseFloat(movement.descuento) > 0) { + console.log('🎯 Found discount in movement.descuento:', movement.descuento); + return true; + } + + // Check for anticipo aplicado in concepto text + if (movement.concepto && movement.concepto.toLowerCase().includes('anticipo aplicado')) { + console.log('🎯 Found anticipo in concepto text'); + return true; + } + + // Check for subtotal vs monto difference (indicating discount) + const subtotal = parseFloat(movement.subtotal || movement.monto || '0'); + const monto = parseFloat(movement.monto || '0'); + if (subtotal > monto && (subtotal - monto) > 0.01) { + console.log('🎯 Found discount from subtotal/monto difference:', subtotal - monto); + return true; + } + + console.log('❌ No discount found in movement'); + return false; + } + + extractDiscountInfo(data) { + // Priority 1: discountInfo structured data + if (data.discountInfo && parseFloat(data.discountInfo.amount || '0') > 0) { + return { + amount: data.discountInfo.amount, + label: data.tipoDescuento ? `Descuento (${data.tipoDescuento})` : 'Descuento', + detail: data.motivoDescuento || null + }; + } + + // Priority 2: direct descuento field + if (data.descuento && parseFloat(data.descuento) > 0) { + return { + amount: data.descuento, + label: data.tipoDescuento ? `Descuento (${data.tipoDescuento})` : 'Descuento', + detail: data.motivoDescuento || null + }; + } + + // Priority 3: Calculate from subtotal vs monto + const subtotal = parseFloat(data.subtotal || data.monto || '0'); + const monto = parseFloat(data.monto || '0'); + if (subtotal > monto && (subtotal - monto) > 0.01) { + const discountAmount = (subtotal - monto).toFixed(2); + return { + amount: discountAmount, + label: 'Anticipo Aplicado', + detail: 'Anticipo manual - no registrado previamente' + }; + } + + // Priority 4: Extract from concepto text if it mentions anticipo + if (data.concepto && data.concepto.toLowerCase().includes('anticipo aplicado')) { + // Try to extract amount from text + const match = data.concepto.match(/\$(\d+(?:\.\d{2})?)/); + if (match) { + return { + amount: match[1], + label: 'Anticipo Aplicado', + detail: 'Anticipo manual' + }; + } + } + + // Fallback + return { + amount: '0.00', + label: 'Descuento', + detail: null + }; + } + + analyzeTicketType(movement) { + console.log('🔍 Analyzing movement for discounts:', { + discountInfo: movement.discountInfo, + descuento: movement.descuento, + subtotal: movement.subtotal, + monto: movement.monto, + concepto: movement.concepto + }); + + const analysis = { + // Basic ticket analysis + hasAppointment: !!(movement.fechaCita && movement.horaCita), + isAnticipo: movement.tipo === 'Anticipo' || + movement.concepto?.toLowerCase().includes('anticipo'), + hasAnticipoApplied: movement.discountInfo?.type === 'anticipo', + hasConsent: movement.client?.consentimiento || movement.client?.esOncologico, + isOncology: movement.client?.esOncologico, + + // Service type detection + isService: movement.tipo === 'service' || movement.tipo === 'Service', + isCourse: movement.tipo === 'course' || movement.tipo === 'Curso', + + // Payment analysis - check multiple fields for discounts/anticipos + hasDiscount: this.hasAnyDiscount(movement), + isWarriorDiscount: movement.discountInfo?.type === 'warrior', + + // Client type analysis + hasClientInfo: !!(movement.client && movement.client.nombre), + hasClientPhone: !!(movement.client && movement.client.telefono), + + // Medical info analysis + hasMedicalInfo: function() { + return this.isOncology && movement.client && ( + movement.client.nombreMedico || + movement.client.telefonoMedico || + movement.client.cedulaMedico + ); + }, + + // Anticipo logic + needsAnticipoNotes: function() { + // Show anticipo notes only for pure anticipos (not applied to services) + return this.isAnticipo && !this.hasAnticipoApplied; + }, + + // Check if service is PMU related + isPMUService: function() { + const pmuKeywords = ['microblading', 'pmu', 'pigmentacion', 'cejas', 'labios', 'vanity brows', 'brows']; + const concepto = movement.concepto?.toLowerCase() || ''; + return pmuKeywords.some(keyword => concepto.includes(keyword)); + }, + + // Determine main ticket category + getTicketCategory: function() { + if (this.isAnticipo && !this.hasAnticipoApplied) return 'anticipo-puro'; + if (this.isService && this.hasAnticipoApplied) return 'servicio-con-anticipo'; + if (this.isService && this.hasConsent && this.isOncology && this.isPMUService()) return 'servicio-oncologico'; + if (this.isService && this.hasConsent && this.isPMUService()) return 'servicio-con-consentimiento'; + if (this.isService && this.hasAppointment) return 'servicio-con-cita'; + if (this.isService) return 'servicio-simple'; + if (this.isCourse) return 'curso'; + return 'otros'; + } + }; + + console.log('🔍 Analysis result - hasDiscount:', analysis.hasDiscount); + console.log('🔍 Analysis result - discountInfo amount:', movement.discountInfo?.amount); + + return analysis; + } + + // Enhanced data mapping system + mapMovementData(movement) { + return { + // Basic info + folio: movement.folio || 'N/A', + fecha: this.formatDate(movement.fecha) || new Date().toLocaleDateString('es-MX'), + concepto: movement.concepto || movement.serviceName || 'Servicio', + monto: this.formatAmount(movement.monto || movement.total || '0'), + metodo: movement.metodo || movement.paymentMethod || 'No especificado', + staff: movement.staff || movement.attendedBy || 'Ale Ponce', + + // Client info + cliente: this.getClientName(movement), + telefonoCliente: this.getClientPhone(movement), + + // Appointment info + fechaCita: movement.fechaCita || movement.appointmentDate, + horaCita: movement.horaCita || movement.appointmentTime, + + // Discount/anticipo info + subtotal: this.formatAmount(movement.subtotal || movement.monto), + descuento: this.formatAmount(movement.discountInfo?.amount || '0'), + tipoDescuento: movement.discountInfo?.type || null, + motivoDescuento: movement.discountInfo?.reason || null, + + // Client object + client: movement.client || {}, + + // Original movement for reference + originalMovement: movement + }; + } + + // Helper functions for data formatting + formatDate(dateStr) { + if (!dateStr) return null; + try { + const date = new Date(dateStr); + if (isNaN(date.getTime())) return dateStr; // Invalid date + return date.toLocaleDateString('es-MX', { + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + } catch { + return dateStr; + } + } + + formatAmount(amount) { + if (!amount) return '0.00'; + const numAmount = typeof amount === 'string' ? parseFloat(amount) : amount; + return numAmount.toFixed(2); + } + + getClientName(movement) { + if (movement.cliente) return movement.cliente; + if (movement.client?.nombre) return movement.client.nombre; + if (movement.clientName) return movement.clientName; + return 'Cliente General'; + } + + getClientPhone(movement) { + if (movement.telefonoCliente) return movement.telefonoCliente; + if (movement.client?.telefono) return movement.client.telefono; + if (movement.clientPhone) return movement.clientPhone; + return null; + } + + // Dynamic template generation based on ticket type + generateReceiptHTML(movement, ticketType) { + const data = this.mapMovementData(movement); + const category = ticketType.getTicketCategory(); + + return ` +
+
+ + ${this.generateMobileHeader()} + ${this.generateMobileTitle()} + ${this.generateMobileBasicInfo(data)} + ${this.generateMobileServicesTable(data, ticketType)} + ${this.generateMobileTotalsSection(data, ticketType)} + ${this.generateCategorySpecificSections(data, ticketType, category)} + ${this.generateMobileFooter()} + +
+
+ `; + } + + generateMobileHeader() { + return ` +
+ ap +
+
ALEJANDRA PONCE
+
BEAUTY EXPERT • MASTER TRAINER
+
+
+ `; + } + + generateHeader() { + return ` +
+
+
ap
+
+
+
ALEJANDRA PONCE
+
BEAUTY EXPERT • MASTER TRAINER
+
+
+ `; + } + + generateMobileTitle() { + return ` +
+ COMPROBANTE DE PAGO +
+ `; + } + + generateTitle() { + return ` +
+

COMPROBANTE DE PAGO

+
+ `; + } + + generateMobileBasicInfo(data) { + return ` +
+
+ Cliente: + ${data.cliente} +
+ ${data.telefonoCliente ? ` +
+ Contacto: + ${data.telefonoCliente} +
+ ` : ''} +
+
+
Folio
+
${data.folio}
+
+
+
Fecha
+
${data.fecha}
+
+
+
+ `; + } + + generateBasicInfo(data) { + return ` +
+
+ Folio + ${data.folio} +
+
+ Fecha + ${data.fecha} +
+
+ `; + } + + generateMobileClientInfo(data, ticketType) { + return ``; // Esta función ya no se usa, la info del cliente se movió a generateMobileBasicInfo + } + + generateClientInfo(data, ticketType) { + return ` +
+
+ Cliente: + ${data.cliente} +
+ ${data.telefonoCliente ? ` +
+ Contacto: + ${data.telefonoCliente} +
+ ` : ''} +
+ `; + } + + generateMobileServicesTable(data, ticketType) { + console.log('🔍 Services table - hasDiscount:', ticketType.hasDiscount); + console.log('🔍 Services table - descuento amount:', data.descuento); + console.log('🔍 Services table - discountInfo:', data.originalMovement?.discountInfo); + return ` +
+ + + + + + + + + + + + + ${ticketType.hasDiscount ? this.generateMobileDiscountTableRow(data) : ''} + +
DescripciónMonto
${data.concepto}$${data.monto}
+
+ `; + } + + generateServicesTable(data, ticketType) { + return ` +
+
+
Description
+
Amount
+
+
+
+
${data.concepto}
+
$${data.monto}
+
+ ${ticketType.hasDiscount ? this.generateDiscountRow(data) : ''} +
+
+ `; + } + + generateMobileDiscountTableRow(data) { + const discountInfo = this.extractDiscountInfo(data); + console.log('🎨 Generating discount row with extracted info:', discountInfo); + + return ` + + + ${discountInfo.label} + ${discountInfo.detail ? `
${discountInfo.detail}` : ''} + + -$${discountInfo.amount} + + `; + } + + generateAppointmentInTotals(data) { + const fechaCita = data.fechaCita ? this.formatDate(data.fechaCita) : null; + const citaText = fechaCita && data.horaCita ? `${fechaCita} - ${data.horaCita}` : 'Por confirmar'; + return ` +
+
🗓️ Tu cita es: ${citaText}
+
+ `; + } + + generateMobileDiscountRow(data) { + const discountInfo = this.extractDiscountInfo(data); + return ` +
+
+
+ ${discountInfo.label} + ${discountInfo.detail ? `
${discountInfo.detail}` : ''} +
+
-$${discountInfo.amount}
+
+
+ `; + } + + generateDiscountRow(data) { + const discountInfo = this.extractDiscountInfo(data); + return ` +
+
+ ${discountInfo.label} + ${discountInfo.detail ? `
${discountInfo.detail}` : ''} +
+
-$${discountInfo.amount}
+
+ `; + } + + generateMobileTotalsSection(data, ticketType) { + return ` +
+
+ Total Pagado + $${data.monto} +
+
+ Método de Pago: + ${data.metodo} +
+
+ Te atendió: + ${data.staff} +
+ ${ticketType.hasAppointment ? this.generateAppointmentInTotals(data) : ''} +
+ `; + } + + generateTotalsSection(data, ticketType) { + return ` +
+
+ Total Paid + $${data.monto} +
+
+ Payment Method: + ${data.metodo} +
+
+ Te atendió: + ${data.staff} +
+
+ `; + } + + generateCategorySpecificSections(data, ticketType, category) { + let sections = ''; + + // Add category-specific content + switch (category) { + case 'anticipo-puro': + sections += this.generateAnticipoSection(); + break; + case 'servicio-oncologico': + sections += this.generateOncologySection(data); + break; + case 'servicio-con-consentimiento': + sections += this.generateConsentSection(); + break; + case 'curso': + sections += this.generateCourseSection(); + break; + } + + return sections; + } + + generateAppointmentSection(data) { + return ` +
+

📅 Información de Cita

+

Fecha: ${data.fechaCita || 'Por confirmar'}

+

Hora: ${data.horaCita || 'Por confirmar'}

+
+ `; + } + + generateAnticipoSection() { + return ` +
+

💰 Notas del Anticipo

+

Al dejar tu anticipo, te agradecemos tu compromiso con nuestro tiempo, de la misma forma en que nosotros respetamos el tuyo.

+

Las cancelaciones con menos de 48 horas no son reembolsables.

+
+ `; + } + + generateConsentSection() { + return ` + + `; + } + + generateOncologySection(data) { + const client = data.client; + return ` +
+

🎗️ Consentimiento Oncológico

+

El cliente declara ser paciente oncológico y que la información de su médico es veraz.

+ ${client.nombreMedico ? `

Médico: ${client.nombreMedico}

` : ''} + ${client.cedulaMedico ? `

Cédula: ${client.cedulaMedico}

` : ''} +

Al consentir el servicio, declara que la información médica proporcionada es veraz.

+
+ `; + } + + generateCourseSection() { + return ` +
+

🎓 Información del Curso

+

✅ Inscripción confirmada al programa educativo

+

Términos y condiciones del curso aceptados

+

Para consultas sobre fechas y material, contactar directamente

+
+ `; + } + + generateMobileFooter() { + return ` + + `; + } + + generateFooter() { + return ` + + `; + } + + async downloadReceiptPNG(movementId, movementData = null) { + try { + console.log('🎨 PNG Receipt Generator - Starting generation for:', movementId); + console.log('📋 Movement data provided:', movementData); + + // Check dependencies + if (typeof html2canvas === 'undefined') { + throw new Error('html2canvas library not loaded'); + } + if (typeof saveAs === 'undefined') { + throw new Error('FileSaver library not loaded'); + } + + // Get movement data - use provided data or fetch test data + const movement = movementData || await this.getMovementById(movementId); + console.log('📄 Final movement data:', movement); + + // Analyze ticket type + const ticketType = this.analyzeTicketType(movement); + console.log('🔍 Ticket type analysis:', ticketType); + + // Generate HTML + const receiptHTML = this.generateReceiptHTML(movement, ticketType); + console.log('🏗️ Generated HTML length:', receiptHTML.length); + + // Create temporary DOM element with mobile dimensions + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = receiptHTML; + tempDiv.style.position = 'absolute'; + tempDiv.style.left = '-9999px'; + tempDiv.style.top = '-9999px'; + tempDiv.style.width = '540px'; + tempDiv.style.height = '960px'; + tempDiv.style.backgroundColor = 'white'; + tempDiv.style.fontFamily = "'Montserrat', sans-serif"; + tempDiv.style.overflow = 'hidden'; + + // Apply receipt styles and ensure CSS is loaded + tempDiv.className = 'receipt-container'; + + // Create and inject CSS link to ensure styles are applied + const cssLink = document.createElement('link'); + cssLink.rel = 'stylesheet'; + cssLink.href = 'receipt.css'; + if (!document.querySelector('link[href="receipt.css"]')) { + document.head.appendChild(cssLink); + } + + // Ensure child element has proper mobile dimensions too + const tempElement = tempDiv.firstElementChild; + if (tempElement) { + tempElement.style.width = '540px'; + tempElement.style.height = '960px'; + tempElement.style.backgroundColor = 'white'; + tempElement.style.fontFamily = "'Montserrat', sans-serif"; + tempElement.style.display = 'block'; + tempElement.style.overflow = 'hidden'; + } + + document.body.appendChild(tempDiv); + console.log('📐 Temp div created and added to DOM with explicit dimensions'); + + // Wait for DOM to settle, CSS to load, and images to load + console.log('⏳ Waiting for DOM to settle, CSS to load, and images to load...'); + await new Promise(resolve => setTimeout(resolve, 1000)); // Longer wait for CSS + await this.waitForImages(tempDiv); + console.log('✅ Images loaded and DOM settled'); + + // Log element dimensions before canvas creation + const rect = tempDiv.getBoundingClientRect(); + const childRect = tempElement?.getBoundingClientRect(); + console.log('📏 Temp div dimensions:', rect?.width, 'x', rect?.height); + console.log('📏 Child element dimensions:', childRect?.width, 'x', childRect?.height); + + // Force a reflow to ensure all dimensions are calculated + tempDiv.offsetHeight; + if (tempElement) tempElement.offsetHeight; + + // Use the element with the most reliable dimensions + const elementToCapture = tempElement || tempDiv; + const finalRect = elementToCapture.getBoundingClientRect(); + console.log('📏 Final element to capture dimensions:', finalRect?.width, 'x', finalRect?.height); + + // Convert to PNG with mobile config + console.log('🖼️ Converting to PNG with html2canvas...'); + const adjustedConfig = { + scale: 2, + width: 540, + height: 960, + backgroundColor: 'white', + useCORS: true, + allowTaint: true, + removeContainer: false + }; + console.log('🔧 Using canvas config:', adjustedConfig); + + const canvas = await html2canvas(elementToCapture, adjustedConfig); + console.log('✅ Canvas created, dimensions:', canvas.width, 'x', canvas.height); + + // Clean up + document.body.removeChild(tempDiv); + console.log('🧹 Temporary DOM element removed'); + + // Download + console.log('💾 Creating blob and downloading...'); + canvas.toBlob(blob => { + if (blob) { + saveAs(blob, `Ticket_${movement.folio}.png`); + console.log('🎉 PNG receipt downloaded successfully:', `Ticket_${movement.folio}.png`); + } else { + throw new Error('Failed to create blob from canvas'); + } + }); + + } catch (error) { + console.error('❌ Error generating receipt:', error); + alert('Error al generar el recibo PNG: ' + error.message); + } + } + + async waitForImages(container) { + const images = container.querySelectorAll('img'); + const imagePromises = Array.from(images).map(img => { + return new Promise((resolve) => { + if (img.complete) { + resolve(); + } else { + img.onload = resolve; + img.onerror = resolve; // Continue even if image fails to load + } + }); + }); + await Promise.all(imagePromises); + } + + async getMovementById(movementId) { + // Placeholder - will integrate with actual movement retrieval + // For now, return different test scenarios based on movementId + + const testScenarios = { + 'TEST-001': { + // Servicio simple + id: movementId, + folio: 'AP-001', + fecha: '2025-09-14', + cliente: 'Ana García', + telefonoCliente: '+52 614 123 4567', + concepto: 'Pestañas Volumen Ruso', + monto: '1200.00', + metodo: 'Efectivo', + staff: 'Ale Ponce', + tipo: 'service', + client: { nombre: 'Ana García', telefono: '+52 614 123 4567' } + }, + + 'TEST-002': { + // Servicio con cita + id: movementId, + folio: 'AP-002', + fecha: '2025-09-14', + cliente: 'María López', + concepto: 'Microblading Completo', + monto: '2500.00', + metodo: 'Tarjeta', + staff: 'Ale Ponce', + fechaCita: '2025-09-20', + horaCita: '10:00', + tipo: 'service', + client: { nombre: 'María López' } + }, + + 'TEST-003': { + // Anticipo puro + id: movementId, + folio: 'AP-003', + fecha: '2025-09-14', + cliente: 'Carmen Ruiz', + concepto: 'Anticipo - Pestañas', + monto: '500.00', + metodo: 'Transferencia', + staff: 'Ale Ponce', + fechaCita: '2025-09-25', + horaCita: '14:00', + tipo: 'Anticipo', + client: { nombre: 'Carmen Ruiz' } + }, + + 'TEST-004': { + // Servicio con anticipo aplicado + id: movementId, + folio: 'AP-004', + fecha: '2025-09-14', + cliente: 'Laura Pérez', + concepto: 'Pestañas Mega Volumen', + monto: '800.00', + subtotal: '1300.00', + metodo: 'Efectivo', + staff: 'Ale Ponce', + tipo: 'service', + discountInfo: { + type: 'anticipo', + amount: '500.00', + reason: 'Anticipo aplicado' + }, + client: { nombre: 'Laura Pérez' } + }, + + 'TEST-005': { + // Paciente oncológico + id: movementId, + folio: 'AP-005', + fecha: '2025-09-14', + cliente: 'Elena Martínez', + concepto: 'Microblading Oncológico', + monto: '0.00', + metodo: 'Cortesía', + staff: 'Ale Ponce', + fechaCita: '2025-09-18', + horaCita: '11:00', + tipo: 'service', + discountInfo: { + type: 'warrior', + amount: '2500.00', + reason: 'Programa Vanity Warriors' + }, + client: { + nombre: 'Elena Martínez', + telefono: '+52 614 987 6543', + consentimiento: true, + esOncologico: true, + nombreMedico: 'Dr. Carlos Hernández', + telefonoMedico: '+52 614 555 0123', + cedulaMedico: 'CED-789456' + } + }, + + 'TEST-006': { + // Curso + id: movementId, + folio: 'AP-006', + fecha: '2025-09-14', + cliente: 'Sofia Vargas', + concepto: 'Curso Básico de Pestañas', + monto: '3500.00', + metodo: 'Transferencia', + staff: 'Ale Ponce', + tipo: 'course', + client: { + nombre: 'Sofia Vargas', + telefono: '+52 614 456 7890', + consentimiento: true + } + } + }; + + // Return test scenario or default + return testScenarios[movementId] || testScenarios['TEST-001']; + } +} + +// Global instance +window.pngReceiptGenerator = new PNGReceiptGenerator(); + +// Global function for easy access +window.downloadReceiptPNG = function(movementId) { + window.pngReceiptGenerator.downloadReceiptPNG(movementId); +}; + +console.log('PNG Receipt Generator initialized successfully (based on broad_idea.png design)'); \ No newline at end of file diff --git a/src/png_ticket/background.png b/src/png_ticket/background.png new file mode 100644 index 0000000..35cc32f Binary files /dev/null and b/src/png_ticket/background.png differ diff --git a/src/png_ticket/background_frame.png b/src/png_ticket/background_frame.png new file mode 100644 index 0000000..e9de4e0 Binary files /dev/null and b/src/png_ticket/background_frame.png differ diff --git a/src/png_ticket/broad_idea.png b/src/png_ticket/broad_idea.png new file mode 100644 index 0000000..0790f74 Binary files /dev/null and b/src/png_ticket/broad_idea.png differ diff --git a/src/png_ticket/isotipo.png b/src/png_ticket/isotipo.png new file mode 100644 index 0000000..861b82f Binary files /dev/null and b/src/png_ticket/isotipo.png differ diff --git a/src/png_ticket/isotipo.svg b/src/png_ticket/isotipo.svg new file mode 100644 index 0000000..0033bdf --- /dev/null +++ b/src/png_ticket/isotipo.svg @@ -0,0 +1,145 @@ + + diff --git a/src/png_ticket/logotipo.png b/src/png_ticket/logotipo.png new file mode 100644 index 0000000..030fa9e Binary files /dev/null and b/src/png_ticket/logotipo.png differ diff --git a/src/png_ticket/logotipo.svg b/src/png_ticket/logotipo.svg new file mode 100644 index 0000000..06b7967 --- /dev/null +++ b/src/png_ticket/logotipo.svg @@ -0,0 +1,387 @@ + + diff --git a/src/png_ticket/payment_text.svg b/src/png_ticket/payment_text.svg new file mode 100644 index 0000000..1b21f7f --- /dev/null +++ b/src/png_ticket/payment_text.svg @@ -0,0 +1,171 @@ + + diff --git a/src/png_ticket/payment_text_1.png b/src/png_ticket/payment_text_1.png new file mode 100644 index 0000000..b44b208 Binary files /dev/null and b/src/png_ticket/payment_text_1.png differ diff --git a/styles.css b/styles.css index 84b208b..58b5945 100644 --- a/styles.css +++ b/styles.css @@ -2309,4 +2309,13 @@ table tbody tr:hover { .action-buttons { flex-direction: column; } -} \ No newline at end of file +}.btn-info { + background-color: #17a2b8; + color: white; + border: 1px solid #17a2b8; + margin-right: 5px; +} +.btn-info:hover { + background-color: #138496; + border-color: #117a8b; +} diff --git a/test-buttons.html b/test-buttons.html new file mode 100644 index 0000000..325f57d --- /dev/null +++ b/test-buttons.html @@ -0,0 +1,212 @@ + + + + + + Test PNG/PDF Buttons + + + + +

🧪 Test PNG/PDF Download Buttons

+ +
+

PNG Receipt Test

+ +
Ready to test...
+
+ +
+

PDF Thermal Ticket Test

+ +
Ready to test...
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/test-receipt-complete.html b/test-receipt-complete.html new file mode 100644 index 0000000..2246532 --- /dev/null +++ b/test-receipt-complete.html @@ -0,0 +1,267 @@ + + + + + + Test Completo PNG Receipt - Todos los Casos + + + + +

🧪 Test Completo del Sistema PNG Receipt

+

Prueba todos los casos de uso del negocio con datos reales.

+ +
+ Status: Inicializando... +
+ +
+
+

🔸 Servicio Simple

+

Cliente: Ana García

+

Servicio: Pestañas Volumen Ruso

+

Monto: $1,200.00

+

Caso básico sin cita ni características especiales.

+ +
+ +
+

📅 Servicio con Cita

+

Cliente: María López

+

Servicio: Microblading Completo

+

Cita: 20 Sep 2025, 10:00

+

Servicio con información de cita programada.

+ +
+ +
+

💰 Anticipo Puro

+

Cliente: Carmen Ruiz

+

Concepto: Anticipo - Pestañas

+

Monto: $500.00

+

Anticipo que se aplicará en servicio futuro. Incluye notas importantes.

+ +
+ +
+

🏷️ Servicio + Anticipo Aplicado

+

Cliente: Laura Pérez

+

Servicio: Pestañas Mega Volumen

+

Descuento: -$500.00 (Anticipo)

+

Servicio con anticipo previamente dado aplicado como descuento.

+ +
+ +
+

🎗️ Paciente Oncológico

+

Cliente: Elena Martínez

+

Servicio: Microblading Oncológico

+

Médico: Dr. Carlos Hernández

+

Programa Vanity Warriors - Incluye información médica completa.

+ +
+ +
+

🎓 Curso/Capacitación

+

Cliente: Sofia Vargas

+

Curso: Curso Básico de Pestañas

+

Monto: $3,500.00

+

Inscripción a programa educativo con términos especiales.

+ +
+
+ +
+

📋 Matriz de Testing

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CasoAnticipoCitaMédicoStatus
Servicio Simple
Servicio + Cita
Anticipo Puro
Servicio + Anticipo✅ (Aplicado)
Paciente Oncológico
Curso
+
+ + + + + + + + + \ No newline at end of file diff --git a/test-receipt.html b/test-receipt.html new file mode 100644 index 0000000..2eed0a9 --- /dev/null +++ b/test-receipt.html @@ -0,0 +1,81 @@ + + + + + + Test PNG Receipt + + + +

Test del Sistema PNG Receipt

+

Este es un test básico para verificar que todo funcione correctamente.

+ + + + + +
+ Status: Listo para probar +
+ + + + + + + + + \ No newline at end of file