import { useState, useRef, useEffect } from 'react'; import './Generator.css'; import { renderQRCode, downloadIDCards, downloadBulkIDCards, getXLSXLib, clientTemplates } from './script/script.jsx'; import { uploadEmployeePhoto, saveClienteProyecto } from './services/storage.js'; import { supabase } from "./services/supabase"; // Convierte datos binarios recibidos (base64, buffer, array de bytes) en un Blob URL local de forma robusta const convertBinaryToBlobUrl = (data) => { if (!data) return null; try { if (typeof data === 'object' && data.type === 'Buffer' && Array.isArray(data.data)) { const byteArray = new Uint8Array(data.data); const blob = new Blob([byteArray], { type: 'image/jpeg' }); return URL.createObjectURL(blob); } if (Array.isArray(data)) { const byteArray = new Uint8Array(data); const blob = new Blob([byteArray], { type: 'image/jpeg' }); return URL.createObjectURL(blob); } if (typeof data === 'string') { const cleanBase64 = data.replace(/^data:image\/\w+;base64,/, ""); const byteCharacters = atob(cleanBase64); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); const blob = new Blob([byteArray], { type: 'image/jpeg' }); return URL.createObjectURL(blob); } } catch (e) { console.error("Error convirtiendo formato binario a Blob URL:", e); } return null; }; export default function IdCardGenerator() { // --- Estados formulario individual ─── const [name, setName] = useState(''); const [role, setRole] = useState(''); const [language, setLanguage] = useState('Esp'); const [selectedClient, setSelectedClient] = useState('Generico'); const [employeeId, setEmployeeId] = useState(''); const [photoSrc, setPhotoSrc] = useState(''); const [photoFile, setPhotoFile] = useState(null); const [photoProcessing, setPhotoProcessing] = useState(false); const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' }); // Bloquea el botón Descargar hasta que nombre, puesto e ID estén llenos const isFormComplete = name.trim() && role.trim() && employeeId.trim(); // --- Estados Lote / Excel Masivo ─── const [bulkEmployees, setBulkEmployees] = useState([]); const [bulkStatusText, setBulkStatusText] = useState(''); const [bulkDownloadStatus, setBulkDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' }); const [showTooltip, setShowTooltip] = useState(false); const fileInputRef = useRef(null); const excelInputRef = useRef(null); const qrCanvasRef = useRef(null); const baseUrl = import.meta.env.BASE_URL.endsWith('/') ? import.meta.env.BASE_URL : `${import.meta.env.BASE_URL}/`; useEffect(() => { if (qrCanvasRef.current) { renderQRCode(employeeId, qrCanvasRef.current); } }, [employeeId]); const handlePhotoUpload = async (e) => { const file = e.target.files[0]; if (!file) return; setPhotoFile(file); setPhotoSrc(URL.createObjectURL(file)); setPhotoProcessing(true); try { const editedBlob = await processPhotoWithAI(file); const editedUrl = URL.createObjectURL(editedBlob); setPhotoSrc(editedUrl); setPhotoFile(new File([editedBlob], file.name, { type: editedBlob.type || 'image/jpeg' })); } catch (error) { console.error('Error al procesar la foto con IA en n8n:', error); alert('No se pudo procesar la foto con IA. Se usará la foto original para la vista previa.'); } finally { setPhotoProcessing(false); } }; const processPhotoWithAI = async (file) => { const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL; const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN; const formData = new FormData(); formData.append('edited', file, file.name); formData.append('name', name); formData.append('role', role); formData.append('employeeId', employeeId); formData.append('fechaProcesado', new Date().toISOString()); const response = await fetch(N8N_WEBHOOK_URL, { method: 'POST', headers: { 'Authorization': TOKEN_SECRETO }, body: formData }); if (!response.ok) { throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`); } const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { const data = await response.json(); const bin = data.edited || data.data || data.foto_processed_base64 || data.photo || data.binary || (Array.isArray(data) && (data[0]?.edited || data[0]?.data)); const blobUrl = convertBinaryToBlobUrl(bin); if (!blobUrl) throw new Error('n8n no devolvió un binario válido.'); const blob = await (await fetch(blobUrl)).blob(); return blob; } const blob = await response.blob(); if (!blob || blob.size === 0) throw new Error('n8n devolvió un binario vacío.'); return blob; }; const handleEmployeeIdChange = (e) => { const rawValue = e.target.value; const cleanValue = rawValue.replace(/[-\s]/g, ''); setEmployeeId(cleanValue); }; const enviarDatosAn8n = async (datosEmpleados) => { const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL; const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN; try { const response = await fetch(N8N_WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': TOKEN_SECRETO }, body: JSON.stringify({ empleados: datosEmpleados, total: datosEmpleados.length, fechaProcesado: new Date().toISOString() }) }); if (!response.ok) { throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`); } const resData = await response.json(); return Array.isArray(resData) ? resData : (resData.empleados || resData.data || []); } catch (error) { console.error('Error de comunicación con n8n:', error); throw error; } }; const handleBulkSupabaseSync = async (cedula, blobData, currentEmployee) => { const cleanCedula = String(cedula).replace(/[-\s]/g, ''); if (!cleanCedula) return; try { // Se usa el cliente extraído directamente del array renderizado const clienteProyectoValue = currentEmployee?.selectedClient || 'Generico'; await saveClienteProyecto(cleanCedula, clienteProyectoValue); console.log(`[Supabase DB] Guardado: ${cleanCedula} → ${clienteProyectoValue}`); const customFile = new File([blobData], `${cleanCedula}.jpg`, { type: 'image/jpeg' }); const remoteStorageUrl = await uploadEmployeePhoto(cleanCedula, customFile); if (remoteStorageUrl) { await supabase.rpc("save_employee_photo", { p_employee_number: cleanCedula, p_photo_url: remoteStorageUrl }); } } catch (error) { console.error(`[Supabase Sync Fallido] Cédula ${cleanCedula}:`, error); } }; const handleExcelUpload = async (e) => { const file = e.target.files[0]; if (!file) return; setBulkStatusText('Leyendo documento...'); try { const XLSXLib = await getXLSXLib(); const reader = new FileReader(); reader.onload = async (evt) => { try { const bstr = evt.target.result; const wb = XLSXLib.read(bstr, { type: 'binary' }); const wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; const jsonRows = XLSXLib.utils.sheet_to_json(ws); if (jsonRows.length === 0) { setBulkStatusText('❌ El archivo está vacío.'); return; } const formatted = jsonRows.map((row) => { let rawLang = String(row.lenguaje || 'Esp').trim().toLowerCase(); if (rawLang.includes('es') || rawLang.includes('esp')) { rawLang = 'Esp'; } else if (rawLang.includes('en') || rawLang.includes('ing')) { rawLang = 'Ing'; } else { rawLang = 'Esp'; } let rawClient = String(row['cliente/proyecto'] || 'Generico') .trim() .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); const lowerClient = rawClient.toLowerCase(); if (lowerClient.includes('nestle')) rawClient = 'Nestle'; else if (lowerClient.includes('claro')) rawClient = 'Claro'; else if (lowerClient.includes('colgate')) rawClient = 'Colgate'; else if (lowerClient.includes('kitchenaid') || lowerClient.includes('kitchen')) rawClient = 'KitchenAid'; else if (lowerClient.includes('kraft')) rawClient = 'Kraft'; else if (lowerClient.includes('motorola')) rawClient = 'Motorola'; else if (lowerClient.includes('p&g') || lowerClient.includes('p y g')) rawClient = 'P&G'; else if (lowerClient.includes('philip') || lowerClient.includes('morris')) rawClient = 'Philip Morris'; else if (lowerClient.includes('whirlpool')) rawClient = 'Whirlpool'; else rawClient = 'Generico'; const cleanedCedula = String(row.cedula || '').replace(/[-\s]/g, ''); return { name: String(row.nombre || '').trim(), role: String(row.puesto || '').trim(), selectedClient: rawClient, language: rawLang, employeeId: cleanedCedula, fotoUrl: String(row.foto_url || '').trim() }; }).filter(emp => emp.name && emp.role && emp.employeeId); setBulkEmployees(formatted); setBulkStatusText(`✅ ¡Cargados ${formatted.length} colaboradores! Listo para procesar.`); setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' }); } catch (innerErr) { console.error(innerErr); setBulkStatusText('❌ Error de lectura. Revisa el formato de columnas.'); } }; reader.readAsBinaryString(file); } catch (err) { console.error(err); setBulkStatusText('❌ Error cargando el motor XLSX.'); } }; const triggerBulkDownload = async () => { if (bulkEmployees.length === 0) return; setBulkDownloadStatus({ processing: true, text: '🔄 Descargando...' }); setBulkStatusText('Descargando retratos...'); try { const resultadosN8n = await enviarDatosAn8n(bulkEmployees); setBulkStatusText('Generando frentes, reversos y empaquetando en carpetas locales...'); const employeesReadyForRender = bulkEmployees.map((originalEmp, index) => { const n8nEmp = resultadosN8n[index] || resultadosN8n.find(e => e.employeeId === originalEmp.employeeId) || {}; const base64Source = n8nEmp.data || n8nEmp.foto_processed_base64 || originalEmp.data; let finalPhotoUrl = convertBinaryToBlobUrl(base64Source) || originalEmp.fotoUrl; const validClient = n8nEmp.selectedClient || originalEmp.selectedClient || 'Generico'; const validLang = n8nEmp.language || originalEmp.language || 'Esp'; return { name: n8nEmp.name || originalEmp.name, role: n8nEmp.role || originalEmp.role, selectedClient: validClient, language: validLang, employeeId: n8nEmp.employeeId || originalEmp.employeeId, fotoUrl: finalPhotoUrl }; }); await downloadBulkIDCards({ employees: employeesReadyForRender, baseUrl, onStateChange: null, // CORRECCIÓN AQUÍ: Buscamos al empleado en nuestro arreglo usando la cédula que nos devuelve script.jsx onProcessEmployeePhoto: (cedula, blobData) => { const currentEmp = employeesReadyForRender.find(emp => emp.employeeId === cedula); return handleBulkSupabaseSync(cedula, blobData, currentEmp); } }); setBulkStatusText('🎉 ¡Lote de carnets guardado correctamente!'); setBulkEmployees([]); } catch (error) { console.error(error); setBulkStatusText(`❌ Error al procesar: ${error.message || error}`); } finally { setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' }); } }; const triggerDownload = async () => { if (!validateForm()) return; setDownloadStatus({ processing: true, text: '🔄 Descargando...' }); try { await saveClienteProyecto(employeeId, selectedClient); if (photoFile) { const photoUrl = await uploadEmployeePhoto(employeeId, photoFile); await supabase.rpc("save_employee_photo", { p_employee_number: employeeId, p_photo_url: photoUrl }); } await downloadIDCards({ name, role, selectedClient, language, photoSrc, baseUrl, qrCanvas: qrCanvasRef.current, onStateChange: setDownloadStatus }); } catch (err) { console.error(err); alert(err.message); setDownloadStatus({ processing: false, text: '⬇ Descargar' }); } }; const validateForm = () => { if (!name.trim()) { alert("Debe ingresar el nombre."); return false; } if (!role.trim()) { alert("Debe ingresar el puesto."); return false; } if (!employeeId.trim()) { alert("Debe ingresar el ID del empleado."); return false; } if (!photoFile) { alert("Debe subir la foto del colaborador."); return false; } return true; }; return ( <>
GomezLee Marketing · Carnet Corporativo CR80