diff --git a/src/Generator.jsx b/src/Generator.jsx index bda92b0..ce28998 100644 --- a/src/Generator.jsx +++ b/src/Generator.jsx @@ -1,28 +1,35 @@ import React, { useState, useRef, useEffect } from 'react'; import './App.css'; -// Importamos las funciones lógicas nativas desde el archivo portado script.jsx -import { renderQRCode, downloadIDCards } from './script/script.jsx'; +import { renderQRCode, downloadIDCards, downloadBulkIDCards, getXLSXLib } from './script/script.jsx'; import { uploadEmployeePhoto } from './services/storage.js'; import { supabase } from "./services/supabase"; export default function IdCardGenerator() { - // --- Estados para manejar la lógica en tiempo real ─── + // --- Estados formulario individual ─── const [name, setName] = useState(''); const [role, setRole] = useState(''); - const [language, setLanguage] = useState('Esp'); // 'Esp' o 'Ing' - const [selectedClient, setSelectedClient] = useState('Generico'); // Sin tildes de forma interna + const [language, setLanguage] = useState('Esp'); + const [selectedClient, setSelectedClient] = useState('Generico'); const [employeeId, setEmployeeId] = useState(''); const [photoSrc, setPhotoSrc] = useState(''); const [photoFile, setPhotoFile] = useState(null); - - // Estado para controlar la UI del botón de descarga mientras procesa const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' }); - // Referencias del DOM controladas por React + // --- 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); - // EFECTO AUTOMÁTICO: Renderiza el QR en tiempo real mientras escribes mediante referencias + // Determinar la URL base de Vite de forma segura (Solo una declaración única aquí) + 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); @@ -31,112 +38,132 @@ export default function IdCardGenerator() { const handlePhotoUpload = (e) => { const file = e.target.files[0]; - if (file) { setPhotoFile(file); - - const localUrl = URL.createObjectURL(file); - setPhotoSrc(localUrl); + setPhotoSrc(URL.createObjectURL(file)); } }; - const handleLanguageChange = (lang) => { - setLanguage(lang); - }; - - const handleClientChange = (e) => { - setSelectedClient(e.target.value); - }; - - // Forzar generación del QR manualmente si se desea mediante el botón verde - const triggerGenerateQR = () => { - if (qrCanvasRef.current) { - renderQRCode(employeeId, qrCanvasRef.current); - } - }; - - // Disparador del motor de renderizado ULTRA MAX QUALITY en sandbox - const triggerDownload = async () => { - if (!validateForm()) { - return; - } + // --- MAPEO DE COLUMNAS CORREGIDO --- + 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(); - // Solo si el usuario seleccionó una foto - if (photoFile) { + reader.onload = (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); - const photoUrl = await uploadEmployeePhoto( - employeeId, - photoFile - ); - - console.log("Foto subida:", photoUrl); - - const { data, error } = await supabase.rpc( - "save_employee_photo", - { - p_employee_number: employeeId, - p_photo_url: photoUrl + if (jsonRows.length === 0) { + setBulkStatusText('❌ El archivo está vacío.'); + return; } - ); - if (error) { - throw error; - } + const formatted = jsonRows.map((row) => { + // 1. Normalizar Idioma (ES/Español -> Esp, EN/Inglés -> Ing) + 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'; + } - if (!data.success) { - console.warn(data.message); - } else { - console.log(data.message); + // 2. CORRECCIÓN: Leer la columna exacta "cliente/proyecto" por separado + let rawClient = String(row['cliente/proyecto'] || 'Generico') + .trim() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, ""); // Convierte "Nestlé" en "Nestle" + + // Mapear nombres específicos para asegurar coincidencia exacta con las llaves de script.jsx + 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'; + + return { + name: String(row.nombre || '').trim(), + role: String(row.puesto || '').trim(), + selectedClient: rawClient, + language: rawLang, + employeeId: String(row.cedula || '').trim(), // CORRECCIÓN: Columna individual 'cedula' + fotoUrl: String(row.foto_url || '').trim() + }; + }).filter(emp => emp.name && emp.role); + + setBulkEmployees(formatted); + setBulkStatusText(`✅ ¡Cargados ${formatted.length} colaboradores con éxito!`); + } 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; + try { + await downloadBulkIDCards({ + employees: bulkEmployees, + baseUrl, + onStateChange: setBulkDownloadStatus + }); + setBulkStatusText('🎉 ¡Lote procesado y guardado!'); + } catch (error) { + console.error(error); + alert("Error procesando las imágenes remotas."); + } + }; + + const triggerDownload = async () => { + if (!validateForm()) return; + try { + 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: import.meta.env.BASE_URL.endsWith('/') - ? import.meta.env.BASE_URL - : `${import.meta.env.BASE_URL}/`, + name, role, selectedClient, language, photoSrc, + baseUrl, qrCanvas: qrCanvasRef.current, onStateChange: setDownloadStatus }); - } catch (err) { console.error(err); alert(err.message); } - }; - const baseUrl = import.meta.env.BASE_URL.endsWith('/') - ? import.meta.env.BASE_URL - : `${import.meta.env.BASE_URL}/`; - 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; - } - + 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; }; @@ -147,83 +174,113 @@ export default function IdCardGenerator() {
GomezLee Marketing · Carnet Corporativo CR80
-