// ── CONFIGURACIÓN DE PLANTILLAS DE CLIENTES ── export const clientTemplates = { 'Esp': { 'Generico': 'images/AF Generico Esp.png', 'Claro': 'images/AF Claro Esp.png', 'Colgate': 'images/AF Colgate Esp.png', 'KitchenAid': 'images/AF KitchenAid Esp.png', 'Kraft': 'images/AF Kraft Esp.png', 'Motorola': 'images/AF Motorola Esp.png', 'Nestle': 'images/AF Nestle Esp.png', 'P&G': 'images/AF P&G Esp.png', 'Philip Morris': 'images/AF Philip Morris Esp.png', 'Whirlpool': 'images/AF Whirlpool Esp.png' }, 'Ing': { 'Generico': 'images/AF Generico Ing.png', 'Claro': 'images/AF Claro Ing.png', 'Colgate': 'images/AF Colgate Ing.png', 'KitchenAid': 'images/AF KitchenAid Ing.png', 'Kraft': 'images/AF Kraft Ing.png', 'Motorola': 'images/AF Motorola Ing.png', 'Nestle': 'images/AF Nestle Ing.png', 'P&G': 'images/AF P&G Ing.png', 'Philip Morris': 'images/AF Philip Morris Ing.png', 'Whirlpool': 'images/AF Whirlpool Ing.png' } }; // ── INYECTORES DINÁMICOS DE DEPENDENCIAS ── function loadScript(src, globalKey) { return new Promise((resolve) => { if (window[globalKey]) return resolve(window[globalKey]); const s = document.createElement('script'); s.src = src; s.onload = () => resolve(window[globalKey]); document.head.appendChild(s); }); } export const getQRLib = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js', 'QRCode'); const getHtml2Canvas = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', 'html2canvas'); export const getJSZip = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js', 'JSZip'); export const getXLSXLib = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js', 'XLSX'); export function convertDriveUrlToDirect(url) { if (!url) return ''; if (!url.includes('drive.google.com') && !url.includes('docs.google.com')) { return url; } try { const regExp = /(?:https?:\/\/)?(?:drive\.google\.com\/)(?:file\/d\/|open\?id=)([\w-]+)/; const match = url.match(regExp); if (match && match[1]) { // Endpoint optimizado para saltar bloqueos pesados de descargas directas anónimas return `https://drive.google.com/thumbnail?id=${match[1]}&sz=w1000`; } } catch (e) { console.error("Error convirtiendo la URL de Drive:", e); } return url; } export async function fetchImageAsBlob(url) { if (!url) return null; if (url.startsWith('blob:') || url.startsWith('data:')) { try { const response = await fetch(url); if (response.ok) return await response.blob(); } catch (e) { console.error("Error al obtener Blob local de URL blob/data:", e); } return null; } if (url.includes('drive.google.com') || url.includes('docs.google.com')) { try { const response = await fetch(url); if (response.ok) return await response.blob(); } catch { console.warn("No se pudo obtener el Blob directo de Drive debido a restricciones CORS."); } return null; } try { const proxyUrl = `https://images.weserv.nl/?url=${encodeURIComponent(url)}&default=${encodeURIComponent(url)}`; const response = await fetch(proxyUrl); if (!response.ok) throw new Error("Error en respuesta de red proxy"); return await response.blob(); } catch (e) { console.error("Error haciendo fetch con bypass CORS a la imagen externa:", e); try { const response = await fetch(url, { mode: 'cors' }); return await response.blob(); } catch { return null; } } } function preloadImage(src) { return new Promise((resolve) => { if (!src) return resolve(null); const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => resolve(src); img.onerror = () => { const retryImg = new Image(); retryImg.onload = () => resolve(src); retryImg.onerror = () => resolve(null); retryImg.src = src; }; img.src = src; }); } // ── GENERADOR DE QR NATIVO (Garantiza ID limpio en URL) ── export async function renderQRCode(employeeId, canvasElement) { if (!canvasElement) return; const cleanId = (employeeId || '').replace(/[-\s]/g, ''); const ctx = canvasElement.getContext('2d'); ctx.clearRect(0, 0, canvasElement.width, canvasElement.height); if (cleanId.length <= 2) return; const QRCodeLib = await getQRLib(); const tmp = document.createElement('div'); tmp.style.display = 'none'; document.body.appendChild(tmp); new QRCodeLib(tmp, { text: `http://localhost:5173/employee/${cleanId}`, width: 55, height: 55, colorDark: '#000000', colorLight: '#ffffff', correctLevel: QRCodeLib.CorrectLevel.M }); await new Promise((resolve) => setTimeout(resolve, 60)); const img = tmp.querySelector('img') || tmp.querySelector('canvas'); const drawIt = (src) => { return new Promise((res) => { const i = new Image(); i.onload = () => { ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, 55, 55); ctx.drawImage(i, 0, 0, 55, 55); if (tmp.parentNode) document.body.removeChild(tmp); res(); }; i.src = src; }); }; if (img && img.tagName === 'CANVAS') { await drawIt(img.toDataURL()); } else if (img) { if (img.complete) await drawIt(img.src); else { await new Promise((res) => { img.onload = async () => { await drawIt(img.src); res(); }; }); } } else { if (tmp.parentNode) document.body.removeChild(tmp); } } // ── ENGINE DE PROCESAMIENTO INDIVIDUAL ── export async function downloadIDCards(config) { const { name, role, selectedClient, language, photoSrc, baseUrl, qrCanvas, onStateChange } = config; if (onStateChange) onStateChange({ processing: true, text: 'Procesando...' }); const html2canvasLib = await getHtml2Canvas(); const cleanName = name.trim().replace(/\s+/g, '') || 'Empleado'; const frontBgUrl = `${baseUrl}images/AF GLM Frente.png`; const currentGroup = clientTemplates[language] || clientTemplates['Esp']; const backBgFile = currentGroup[selectedClient] || 'images/AF Generico Esp.png'; const backBgUrl = `${baseUrl}${backBgFile}`; const sandbox = document.createElement('div'); sandbox.style.position = 'fixed'; sandbox.style.top = '-9999px'; sandbox.style.left = '-9999px'; sandbox.style.width = '1400px'; document.body.appendChild(sandbox); const cardStyleBase = ` position: relative; width: 650px; height: 1004px; border-radius: 16px; background-size: cover; background-position: center; background-repeat: no-repeat; display: inline-block; margin-right: 50px; overflow: hidden; font-family: Arial, Helvetica, sans-serif; `; const vFront = document.createElement('div'); vFront.style.cssText = cardStyleBase; vFront.style.backgroundImage = `url('${frontBgUrl}')`; let photoTagHtml = `
`; if (photoSrc) { photoTagHtml = ``; } vFront.innerHTML = `
${photoTagHtml}
${name.toUpperCase() || 'NOMBRE APELLIDO'}
${role.toUpperCase() || 'PUESTO'}
`; const vBack = document.createElement('div'); vBack.style.cssText = cardStyleBase; vBack.style.backgroundImage = `url('${backBgUrl}')`; const vQRZone = document.createElement('div'); vQRZone.style.cssText = `position: absolute; bottom: 27px; right: 27px; background: #ffffff; padding: 11px; border-radius: 16px; display: flex; align-items: center; justify-content: center; z-index: 5;`; const vQRCanvas = document.createElement('canvas'); vQRCanvas.width = 149; vQRCanvas.height = 149; vQRCanvas.style.width = '100px'; vQRCanvas.style.height = '100px'; const vQRContext = vQRCanvas.getContext('2d'); if (vQRContext && qrCanvas) { vQRContext.imageSmoothingEnabled = false; vQRContext.drawImage(qrCanvas, 0, 0, 149, 149); } vQRZone.appendChild(vQRCanvas); vBack.appendChild(vQRZone); sandbox.appendChild(vFront); sandbox.appendChild(vBack); const renderOpts = { scale: 1, useCORS: true, allowTaint: false, backgroundColor: null, logging: false, width: 650, height: 1004 }; try { const frontCanvas = await html2canvasLib(vFront, renderOpts); executeFileDownload(frontCanvas.toDataURL('image/png'), `${cleanName}Frente.png`); const backCanvas = await html2canvasLib(vBack, renderOpts); executeFileDownload(backCanvas.toDataURL('image/png'), `${cleanName}Reverso.png`); } catch (error) { console.error('Error generando las vistas en alta resolución:', error); } finally { if (sandbox.parentNode) document.body.removeChild(sandbox); if (onStateChange) onStateChange({ processing: false, text: '⬇ Descargar' }); } } // ── ENGINE MASIVO CON SOLUCIÓN TOTAL POR INYECCIÓN DE IMG CORS NATIVA ── export async function downloadBulkIDCards({ employees, baseUrl, onStateChange, onProcessEmployeePhoto }) { if (onStateChange) onStateChange({ processing: true, text: 'Descargando...' }); const html2canvasLib = await getHtml2Canvas(); const JSZipLib = await getJSZip(); const zip = new JSZipLib(); const hiddenQRCanvas = document.createElement('canvas'); hiddenQRCanvas.width = 55; hiddenQRCanvas.height = 55; const sandbox = document.createElement('div'); sandbox.style.position = 'fixed'; sandbox.style.top = '-9999px'; sandbox.style.left = '-9999px'; sandbox.style.width = '1400px'; document.body.appendChild(sandbox); const cardStyleBase = ` position: relative; width: 650px; height: 1004px; border-radius: 16px; background-size: cover; background-position: center; background-repeat: no-repeat; display: inline-block; overflow: hidden; font-family: Arial, Helvetica, sans-serif; `; const renderOpts = { scale: 1, useCORS: true, allowTaint: false, backgroundColor: null, logging: false, width: 650, height: 1004 }; for (let i = 0; i < employees.length; i++) { const emp = employees[i]; const currentClient = emp.selectedClient || 'Generico'; const sanitizedEmpName = (emp.name || '').trim().replace(/[\\/:*?"<>|]/g, '-') || `Empleado_${i + 1}`; const sanitizedClientName = String(currentClient).trim().replace(/[\\/:*?"<>|]/g, '-'); const folderName = `${sanitizedEmpName} - ${sanitizedClientName}`; const strictCleanId = String(emp.employeeId || '').replace(/[-\s]/g, ''); await renderQRCode(strictCleanId, hiddenQRCanvas); const directDriveUrl = convertDriveUrlToDirect(emp.fotoUrl); let validatedPhotoUrl = null; if (directDriveUrl && strictCleanId) { try { const blobData = await fetchImageAsBlob(directDriveUrl); if (blobData) { validatedPhotoUrl = URL.createObjectURL(blobData); if (onProcessEmployeePhoto) { await onProcessEmployeePhoto(strictCleanId, blobData, emp.selectedClient || ''); } } } catch (err) { console.error(`Error guardando foto en Supabase para Cédula: ${strictCleanId}`, err); } } if (!validatedPhotoUrl && directDriveUrl) { validatedPhotoUrl = await preloadImage(directDriveUrl); } // --- Renderizar Frente Virtual --- const vFront = document.createElement('div'); vFront.style.cssText = cardStyleBase; vFront.style.backgroundImage = `url('${baseUrl}images/AF GLM Frente.png')`; // CAMBIO ESTRATÉGICO AQUÍ: Usamos una etiqueta HTML real con crossorigin explícito, // esto destruye el bloqueo "Anti-Taint" de los lienzos y obliga a html2canvas a procesar la foto de Drive. let photoTagHtml = `
`; if (validatedPhotoUrl) { photoTagHtml = ``; } vFront.innerHTML = `
${photoTagHtml}
${emp.name.toUpperCase()}
${emp.role.toUpperCase()}
`; sandbox.appendChild(vFront); // --- Renderizar Reverso Virtual --- const currentGroup = clientTemplates[emp.language] || clientTemplates['Esp']; const backBgFile = currentGroup[currentClient] || currentGroup['Generico']; const backBgUrl = `${baseUrl}${backBgFile}`; const vBack = document.createElement('div'); vBack.style.cssText = cardStyleBase; vBack.style.backgroundImage = `url('${backBgUrl}')`; const vQRZone = document.createElement('div'); vQRZone.style.cssText = `position: absolute; bottom: 27px; right: 27px; background: #ffffff; padding: 11px; border-radius: 16px; display: flex; align-items: center; justify-content: center; z-index: 5;`; const vQRCanvas = document.createElement('canvas'); vQRCanvas.width = 149; vQRCanvas.height = 149; vQRCanvas.style.width = '100px'; vQRCanvas.style.height = '100px'; const vQRContext = vQRCanvas.getContext('2d'); if (vQRContext) { vQRContext.imageSmoothingEnabled = false; vQRContext.drawImage(hiddenQRCanvas, 0, 0, 149, 149); } vQRZone.appendChild(vQRCanvas); vBack.appendChild(vQRZone); sandbox.appendChild(vBack); // Forzar un retraso mínimo de microsegundos para asegurar el render interno await new Promise((resolve) => setTimeout(resolve, 30)); // Capturas gráficas del ZIP const frontCanvas = await html2canvasLib(vFront, renderOpts); const frontData = frontCanvas.toDataURL('image/png').split(',')[1]; zip.file(`${folderName}/Frente.png`, frontData, { base64: true }); const backCanvas = await html2canvasLib(vBack, renderOpts); const backData = backCanvas.toDataURL('image/png').split(',')[1]; zip.file(`${folderName}/Reverso.png`, backData, { base64: true }); if (validatedPhotoUrl && validatedPhotoUrl.startsWith('blob:')) { URL.revokeObjectURL(validatedPhotoUrl); } sandbox.removeChild(vFront); sandbox.removeChild(vBack); } if (sandbox.parentNode) document.body.removeChild(sandbox); const content = await zip.generateAsync({ type: 'blob' }); const link = document.createElement('a'); link.href = URL.createObjectURL(content); link.download = `Lote_Carnets_GLM.zip`; document.body.appendChild(link); link.click(); document.body.removeChild(link); if (onStateChange) onStateChange({ processing: false, text: '⬇ Descargar' }); } function executeFileDownload(dataUrl, fileName) { const link = document.createElement('a'); link.href = dataUrl; link.download = fileName; document.body.appendChild(link); link.click(); document.body.removeChild(link); }