From 4ea71b7300e20f983ac7bd394fa652cb7393b6d7 Mon Sep 17 00:00:00 2001 From: EidanThen Date: Tue, 30 Jun 2026 16:18:14 -0400 Subject: [PATCH] feat: add IdCardGenerator component with bulk upload and Supabase sync support --- src/Generator.jsx | 53 ++++++++++++++++++++++++++++++---------- src/script/script.jsx | 56 +++++++++++++++++++++++++++++++++---------- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/Generator.jsx b/src/Generator.jsx index ce28998..a5e5c04 100644 --- a/src/Generator.jsx +++ b/src/Generator.jsx @@ -25,7 +25,6 @@ export default function IdCardGenerator() { const excelInputRef = useRef(null); const qrCanvasRef = useRef(null); - // 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}/`; @@ -44,7 +43,36 @@ export default function IdCardGenerator() { } }; - // --- MAPEO DE COLUMNAS CORREGIDO --- + // --- ALMACENAMIENTO AUTOMÁTICO EN EL STORAGE E INSTANCIACIÓN RPC --- + const handleBulkSupabasePhotoSync = async (cedula, blobData) => { + try { + // Creamos un archivo sintáctico válido con tipo explícito image/jpeg + const customFile = new File([blobData], `${cedula}.jpg`, { type: 'image/jpeg' }); + + // Subida al Storage Bucket + const remoteStorageUrl = await uploadEmployeePhoto(cedula, customFile); + + if (remoteStorageUrl) { + // LLamado a tu función RPC nativa + const { data, error } = await supabase.rpc("save_employee_photo", { + p_employee_number: cedula, + p_photo_url: remoteStorageUrl + }); + + if (error) { + console.warn(`[Supabase RPC Error] Cédula ${cedula}:`, error.message); + } else if (data && data.success === false) { + console.log(`[Supabase Info] Cédula ${cedula}: ${data.message}`); + } else { + console.log(`[Supabase Success] Foto sincronizada para la Cédula: ${cedula}`); + } + } + } catch (error) { + console.error(`Sincronización abortada para la cédula ${cedula}:`, error); + } + }; + + // --- MAPEO DE COLUMNAS EXCEL CORREGIDO --- const handleExcelUpload = async (e) => { const file = e.target.files[0]; if (!file) return; @@ -68,7 +96,6 @@ export default function IdCardGenerator() { } 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'; @@ -78,14 +105,13 @@ export default function IdCardGenerator() { rawLang = 'Esp'; } - // 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" + .replace(/[\u0300-\u036f]/g, ""); - // Mapear nombres específicos para asegurar coincidencia exacta con las llaves de script.jsx const lowerClient = rawClient.toLowerCase(); + // CORRECCIÓN ESENCIAL: Dejar las llaves igual a script.jsx (Claro, Nestle, Whirlpool) sin .toUpperCase() if (lowerClient.includes('nestle')) rawClient = 'Nestle'; else if (lowerClient.includes('claro')) rawClient = 'Claro'; else if (lowerClient.includes('colgate')) rawClient = 'Colgate'; @@ -102,7 +128,7 @@ export default function IdCardGenerator() { role: String(row.puesto || '').trim(), selectedClient: rawClient, language: rawLang, - employeeId: String(row.cedula || '').trim(), // CORRECCIÓN: Columna individual 'cedula' + employeeId: String(row.cedula || '').trim(), fotoUrl: String(row.foto_url || '').trim() }; }).filter(emp => emp.name && emp.role); @@ -127,9 +153,10 @@ export default function IdCardGenerator() { await downloadBulkIDCards({ employees: bulkEmployees, baseUrl, - onStateChange: setBulkDownloadStatus + onStateChange: setBulkDownloadStatus, + onProcessEmployeePhoto: handleBulkSupabasePhotoSync }); - setBulkStatusText('🎉 ¡Lote procesado y guardado!'); + setBulkStatusText('🎉 ¡Lote guardado y sincronizado con Supabase!'); } catch (error) { console.error(error); alert("Error procesando las imágenes remotas."); @@ -176,7 +203,7 @@ export default function IdCardGenerator() {
- {/* FORMULARIO MASIVO EXCEL (IZQUIERDA) */} + {/* FORMULARIO MASIVO EXCEL */}
@@ -209,7 +236,7 @@ export default function IdCardGenerator() { }}> Formato de columnas requeridas en el Excel:
nombre, puesto, cliente/proyecto, cedula, lenguaje, foto_url.
- Las imágenes se descargarán automáticamente desde los enlaces públicos de Google Drive. + Las imágenes se guardarán automáticamente en Supabase si no tienen una previa asignada.
)}
@@ -247,7 +274,7 @@ export default function IdCardGenerator() {
- {/* FORMULARIO INDIVIDUAL (CENTRO) */} + {/* FORMULARIO INDIVIDUAL */}
@@ -318,7 +345,7 @@ export default function IdCardGenerator() {
- {/* VISTA PREVIA (DERECHA) */} + {/* VISTA PREVIA */}
Frente
diff --git a/src/script/script.jsx b/src/script/script.jsx index 8310fc0..95d1c45 100644 --- a/src/script/script.jsx +++ b/src/script/script.jsx @@ -42,7 +42,7 @@ const getHtml2Canvas = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/ 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'); -function convertDriveUrlToDirect(url) { +export function convertDriveUrlToDirect(url) { if (!url) return ''; const regExp = /(?:https?:\/\/)?(?:drive\.google\.com\/)(?:file\/d\/|open\?id=)([\w-]+)/; const match = url.match(regExp); @@ -52,6 +52,27 @@ function convertDriveUrlToDirect(url) { return url; } +// Descarga la imagen evadiendo las restricciones CORS de Google Drive usando un CDN/Proxy transparente +export async function fetchImageAsBlob(url) { + if (!url) return null; + try { + // Codificamos la URL directa de drive y la pasamos por el proxy weserv.nl para saltar el CORS de manera segura + 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:", e); + try { + // Intento secundario fallback por si falla el proxy + const response = await fetch(url, { mode: 'cors' }); + return await response.blob(); + } catch (err) { + return null; + } + } +} + function preloadImage(src) { return new Promise((resolve) => { if (!src) return resolve(null); @@ -226,7 +247,7 @@ export async function downloadIDCards(config) { } // ── ENGINE MASIVO CON CORRESPONDENCIA DINÁMICA DE REVERSO POR IDIOMA ── -export async function downloadBulkIDCards({ employees, baseUrl, onStateChange }) { +export async function downloadBulkIDCards({ employees, baseUrl, onStateChange, onProcessEmployeePhoto }) { if (onStateChange) onStateChange({ processing: true, text: 'Descargando...' }); const html2canvasLib = await getHtml2Canvas(); @@ -261,14 +282,26 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange }) for (let i = 0; i < employees.length; i++) { const emp = employees[i]; + const currentClient = emp.selectedClient || 'Generico'; const cleanName = emp.name.trim().replace(/\s+/g, '') || `Empleado_${i + 1}`; - const cleanClient = emp.selectedClient.trim().replace(/\s+/g, '') || 'Generico'; - - const folderName = `${cleanName}_${cleanClient}`; + const folderName = `${cleanName}_${currentClient}`; await renderQRCode(emp.employeeId, hiddenQRCanvas); const directDriveUrl = convertDriveUrlToDirect(emp.fotoUrl); + + // --- PROCESAMIENTO E INYECCIÓN BINARIA A SUPABASE --- + if (onProcessEmployeePhoto && directDriveUrl && emp.employeeId) { + try { + const blobData = await fetchImageAsBlob(directDriveUrl); + if (blobData) { + await onProcessEmployeePhoto(emp.employeeId, blobData); + } + } catch (err) { + console.error(`Error guardando foto en Supabase para Cédula: ${emp.employeeId}`, err); + } + } + const validatedPhotoUrl = await preloadImage(directDriveUrl); // --- Renderizar Frente Virtual --- @@ -302,12 +335,9 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange }) `; sandbox.appendChild(vFront); - // --- Renderizar Reverso Virtual (Asignación exacta e idéntica según cliente e idioma) ─── + // --- Renderizar Reverso Virtual --- const currentGroup = clientTemplates[emp.language] || clientTemplates['Esp']; - - // Comprobación robusta de la existencia de la propiedad mapeada del cliente - const clientKey = currentGroup.hasOwnProperty(emp.selectedClient) ? emp.selectedClient : 'Generico'; - const backBgFile = currentGroup[clientKey]; + const backBgFile = currentGroup[currentClient] || currentGroup['Generico']; const backBgUrl = `${baseUrl}${backBgFile}`; const vBack = document.createElement('div'); @@ -331,14 +361,14 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange }) vBack.appendChild(vQRZone); sandbox.appendChild(vBack); - // Captura de datos gráficos + // Capturas gráficas del ZIP const frontCanvas = await html2canvasLib(vFront, renderOpts); const frontData = frontCanvas.toDataURL('image/png').split(',')[1]; - zip.file(`${folderName}/${cleanName}_${cleanClient}_Frente.png`, frontData, { base64: true }); + zip.file(`${folderName}/${cleanName}_${currentClient}_Frente.png`, frontData, { base64: true }); const backCanvas = await html2canvasLib(vBack, renderOpts); const backData = backCanvas.toDataURL('image/png').split(',')[1]; - zip.file(`${folderName}/${cleanName}_${cleanClient}_Reverso.png`, backData, { base64: true }); + zip.file(`${folderName}/${cleanName}_${currentClient}_Reverso.png`, backData, { base64: true }); sandbox.removeChild(vFront); sandbox.removeChild(vBack);