diff --git a/package-lock.json b/package-lock.json index 0a4d929..be8a727 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,13 @@ "version": "0.0.0", "dependencies": { "@supabase/supabase-js": "^2.108.2", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", + "lucide-react": "^1.23.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -1039,6 +1043,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1147,6 +1160,28 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1167,6 +1202,24 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1491,6 +1544,12 @@ "node": ">=16.0.0" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -1529,6 +1588,15 @@ "dev": true, "license": "ISC" }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1616,6 +1684,12 @@ "node": ">= 4" } }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -1626,6 +1700,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1649,6 +1729,12 @@ "node": ">=0.10.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1710,6 +1796,18 @@ "node": ">=6" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -1734,6 +1832,15 @@ "node": ">= 0.8.0" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -2033,6 +2140,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz", + "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -2142,6 +2258,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2221,6 +2343,12 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2290,6 +2418,21 @@ "react-dom": ">=18" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/rolldown": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", @@ -2324,6 +2467,12 @@ "@rolldown/binding-win32-x64-msvc": "1.1.3" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -2346,6 +2495,12 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2379,6 +2534,27 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -2456,6 +2632,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", @@ -2550,6 +2732,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -2560,6 +2760,27 @@ "node": ">=0.10.0" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index ee39097..ad8923f 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,13 @@ }, "dependencies": { "@supabase/supabase-js": "^2.108.2", + "file-saver": "^2.0.5", + "jszip": "^3.10.1", + "lucide-react": "^1.23.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.1" + "react-router-dom": "^7.18.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/src/App.css b/src/Generator.css similarity index 100% rename from src/App.css rename to src/Generator.css diff --git a/src/Generator.jsx b/src/Generator.jsx index 4de464f..b0647af 100644 --- a/src/Generator.jsx +++ b/src/Generator.jsx @@ -1,9 +1,48 @@ -import React, { useState, useRef, useEffect } from 'react'; -import './App.css'; +import { useState, useRef, useEffect } from 'react'; +import './Generator.css'; import { renderQRCode, downloadIDCards, downloadBulkIDCards, getXLSXLib } from './script/script.jsx'; import { uploadEmployeePhoto } 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 { + // Si es un objeto tipo Buffer de Node.js/n8n (p. ej., { type: 'Buffer', data: [...] }) + 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); + } + + // Si ya es un array de números (bytes) + if (Array.isArray(data)) { + const byteArray = new Uint8Array(data); + const blob = new Blob([byteArray], { type: 'image/jpeg' }); + return URL.createObjectURL(blob); + } + + // Si es un string + if (typeof data === 'string') { + // Limpiar prefijo data URL si existe + const cleanBase64 = data.replace(/^data:image\/\w+;base64,/, ""); + + // Decodificar 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 al convertir datos binarios a Blob URL:", e); + } + return null; +}; + export default function IdCardGenerator() { // --- Estados formulario individual ─── const [name, setName] = useState(''); @@ -18,7 +57,7 @@ export default function IdCardGenerator() { // --- Estados Lote / Excel Masivo ─── const [bulkEmployees, setBulkEmployees] = useState([]); const [bulkStatusText, setBulkStatusText] = useState(''); - const [bulkDownloadStatus, setBulkDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' }); + const [bulkDownloadStatus, setBulkDownloadStatus] = useState({ processing: false, text: '⬇ Descargar Carnets' }); const [showTooltip, setShowTooltip] = useState(false); const fileInputRef = useRef(null); @@ -52,6 +91,62 @@ export default function IdCardGenerator() { setEmployeeId(cleanValue); }; + // --- Envío directo a n8n para recuperar las fotos procesadas --- + 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) { + console.error('Error en n8n:', response.status); + throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`); + } + + const rawData = await response.json(); + console.log('Respuesta cruda de n8n:', rawData); + + let dataArray = []; + if (Array.isArray(rawData)) { + dataArray = rawData; + } else if (rawData && typeof rawData === 'object') { + const keys = ['empleados', 'data', 'result', 'employees', 'items', 'output']; + for (const key of keys) { + if (Array.isArray(rawData[key])) { + dataArray = rawData[key]; + break; + } + } + if (dataArray.length === 0 && (rawData.name || rawData.employeeId || rawData.data || rawData.json)) { + dataArray = [rawData]; + } + } + + // Normalizar el envoltorio `{ json: ... }` si viene de n8n + return dataArray.map(item => { + if (item && item.json && typeof item.json === 'object') { + return item.json; + } + return item; + }); + } catch (error) { + console.error('Error de red enviando a n8n:', error); + throw error; + } + }; + // --- ALMACENAMIENTO AUTOMÁTICO EN EL STORAGE E INSTANCIACIÓN RPC --- const handleBulkSupabasePhotoSync = async (cedula, blobData) => { const cleanCedula = String(cedula).replace(/[-\s]/g, ''); @@ -78,7 +173,7 @@ export default function IdCardGenerator() { } }; - // --- MAPEO DE COLUMNAS EXCEL CON LIMPIEZA DE CÉDULA --- + // --- MAPEO DE COLUMNAS EXCEL --- const handleExcelUpload = async (e) => { const file = e.target.files[0]; if (!file) return; @@ -88,7 +183,7 @@ export default function IdCardGenerator() { const XLSXLib = await getXLSXLib(); const reader = new FileReader(); - reader.onload = (evt) => { + reader.onload = async (evt) => { try { const bstr = evt.target.result; const wb = XLSXLib.read(bstr, { type: 'binary' }); @@ -141,7 +236,9 @@ export default function IdCardGenerator() { }).filter(emp => emp.name && emp.role && emp.employeeId); setBulkEmployees(formatted); - setBulkStatusText(`✅ ¡Cargados ${formatted.length} colaboradores con éxito!`); + setBulkStatusText(`✅ ¡Cargados ${formatted.length} colaboradores! Listo para procesar.`); + setBulkDownloadStatus({ processing: false, text: '⬇ Descargar Carnets' }); + } catch (innerErr) { console.error(innerErr); setBulkStatusText('❌ Error de lectura. Revisa el formato de columnas.'); @@ -154,19 +251,102 @@ export default function IdCardGenerator() { } }; + // --- ACCIÓN DEL BOTÓN CORREGIDA CON FINALLY Y MAPEO TOTAL DE VARIABLES --- const triggerBulkDownload = async () => { if (bulkEmployees.length === 0) return; + + setBulkDownloadStatus({ processing: true, text: '🔄 Descargando...' }); + setBulkStatusText('Conectando con n8n y descargando retratos optimizados por IA...'); + try { + // 1. Obtenemos las imágenes crudas en Base64/Binario desde el webhook de n8n + const resultadosN8n = await enviarDatosAn8n(bulkEmployees); + + if (!resultadosN8n || resultadosN8n.length === 0) { + throw new Error('No se recibieron colaboradores procesados desde n8n.'); + } + + setBulkStatusText('Generando frentes, reversos y empaquetando en carpetas locales...'); + + // 2. Mapeamos garantizando que TODAS las propiedades originales existan para script.jsx + const employeesReadyForRender = resultadosN8n.map((n8nEmp, index) => { + // Buscamos la fila correspondiente en el Excel original (por employeeId o nombre) para mantener cliente + const n8nId = String(n8nEmp.employeeId || n8nEmp.employee_id || n8nEmp.cedula || n8nEmp.id || '').replace(/[-\s]/g, ''); + const n8nName = String(n8nEmp.name || n8nEmp.nombre || '').trim().toLowerCase(); + + const originalEmp = bulkEmployees.find(e => { + const origId = String(e.employeeId).replace(/[-\s]/g, ''); + const origName = String(e.name).trim().toLowerCase(); + return (n8nId && origId === n8nId) || (n8nName && origName === n8nName); + }) || bulkEmployees[index] || {}; + + // Mapeo/limpieza de cliente/proyecto + let rawClient = n8nEmp.selectedClient || n8nEmp.client || n8nEmp.proyecto || n8nEmp['cliente/proyecto'] || originalEmp.selectedClient || 'Generico'; + const lowerClient = String(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'; + + // Mapeo/conversión de la imagen del empleado + let localPhotoUrl = originalEmp.fotoUrl || ''; + const imageData = n8nEmp.data || n8nEmp.foto_processed_base64 || n8nEmp.binary || n8nEmp.image || originalEmp.data; + + if (imageData) { + const blobUrl = convertBinaryToBlobUrl(imageData); + if (blobUrl) { + localPhotoUrl = blobUrl; + } + } + + // Normalización de idioma + let rawLang = String(n8nEmp.language || n8nEmp.lenguaje || n8nEmp.idioma || originalEmp.language || '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'; + } + + // Inyectamos las claves exactas que script.jsx desestructura + return { + name: String(n8nEmp.name || n8nEmp.nombre || originalEmp.name || '').trim(), + role: String(n8nEmp.role || n8nEmp.puesto || originalEmp.role || '').trim(), + selectedClient: rawClient, + language: rawLang, + employeeId: n8nId || String(originalEmp.employeeId || '').replace(/[-\s]/g, ''), + fotoUrl: localPhotoUrl + }; + }).filter(emp => emp.name && emp.employeeId); + + if (employeesReadyForRender.length === 0) { + throw new Error('Ningún colaborador posee datos válidos de nombre e identificación para procesar.'); + } + + // 3. Ejecutamos tu motor original en script.jsx que procesará los diseños de Claro, Nestle, etc. await downloadBulkIDCards({ - employees: bulkEmployees, + employees: employeesReadyForRender, baseUrl, - onStateChange: setBulkDownloadStatus, + onStateChange: null, // Evitamos sobreescritura conflictiva de estados dentro del script onProcessEmployeePhoto: handleBulkSupabasePhotoSync }); - setBulkStatusText('🎉 ¡Lote guardado!'); + + setBulkStatusText('🎉 ¡Lote de carnets guardado correctamente por carpetas!'); + setBulkEmployees([]); + } catch (error) { console.error(error); - alert("Error procesando las imágenes remotas."); + setBulkStatusText(`❌ Error al procesar: ${error.message || error}`); + } finally { + // El bloque finally se ejecuta SIEMPRE (tenga éxito o falle), liberando el botón de forma segura + setBulkDownloadStatus({ processing: false, text: '⬇ Descargar Carnets' }); } }; diff --git a/src/script/script.jsx b/src/script/script.jsx index 084fb02..983cda1 100644 --- a/src/script/script.jsx +++ b/src/script/script.jsx @@ -65,11 +65,21 @@ export function convertDriveUrlToDirect(url) { export async function fetchImageAsBlob(url) { if (!url) return null; - if (url.includes('drive.google.com') || url.includes('docs.google.com')) { + 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; @@ -85,7 +95,7 @@ export async function fetchImageAsBlob(url) { try { const response = await fetch(url, { mode: 'cors' }); return await response.blob(); - } catch (err) { + } catch { return null; } } @@ -303,8 +313,9 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange, o 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 folderName = `${cleanName}_${currentClient}`; + 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, ''); @@ -393,11 +404,11 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange, o // Capturas gráficas del ZIP const frontCanvas = await html2canvasLib(vFront, renderOpts); const frontData = frontCanvas.toDataURL('image/png').split(',')[1]; - zip.file(`${folderName}/${cleanName}_${currentClient}_Frente.png`, frontData, { base64: true }); + 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}/${cleanName}_${currentClient}_Reverso.png`, backData, { base64: true }); + zip.file(`${folderName}/Reverso.png`, backData, { base64: true }); if (validatedPhotoUrl && validatedPhotoUrl.startsWith('blob:')) { URL.revokeObjectURL(validatedPhotoUrl);