diff --git a/src/App.css b/src/App.css
index 7de3b42..d99dd91 100644
--- a/src/App.css
+++ b/src/App.css
@@ -45,6 +45,7 @@ body {
justify-content: center;
align-items: flex-start;
flex-direction: row;
+ margin-top: 30px
}
/* ── CONTROLS PANEL ── */
@@ -255,11 +256,11 @@ body {
/* ── FRONT DYNAMIC OVERLAYS ── */
.front-photo-frame {
position: absolute;
- top: 99px;
- left: 50%;
+ top: 98.5px;
+ left: 49.9%;
transform: translateX(-50%);
- width: 145px;
- height: 145px;
+ width: 145.5px;
+ height: 145.5px;
border-radius: 50%;
overflow: hidden;
display: flex;
diff --git a/src/Generator.jsx b/src/Generator.jsx
index a5e5c04..4de464f 100644
--- a/src/Generator.jsx
+++ b/src/Generator.jsx
@@ -29,6 +29,7 @@ export default function IdCardGenerator() {
? import.meta.env.BASE_URL
: `${import.meta.env.BASE_URL}/`;
+ // Al limpiar el input en tiempo real, employeeId ya vendrá totalmente limpio
useEffect(() => {
if (qrCanvasRef.current) {
renderQRCode(employeeId, qrCanvasRef.current);
@@ -43,36 +44,41 @@ export default function IdCardGenerator() {
}
};
+ // Interceptor para bloquear en tiempo real guiones y espacios en blanco
+ const handleEmployeeIdChange = (e) => {
+ const rawValue = e.target.value;
+ // Reemplaza instantáneamente cualquier guión o espacio por vacío
+ const cleanValue = rawValue.replace(/[-\s]/g, '');
+ setEmployeeId(cleanValue);
+ };
+
// --- ALMACENAMIENTO AUTOMÁTICO EN EL STORAGE E INSTANCIACIÓN RPC ---
const handleBulkSupabasePhotoSync = async (cedula, blobData) => {
+ const cleanCedula = String(cedula).replace(/[-\s]/g, '');
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);
+ const customFile = new File([blobData], `${cleanCedula}.jpg`, { type: 'image/jpeg' });
+ const remoteStorageUrl = await uploadEmployeePhoto(cleanCedula, customFile);
if (remoteStorageUrl) {
- // LLamado a tu función RPC nativa
const { data, error } = await supabase.rpc("save_employee_photo", {
- p_employee_number: cedula,
+ p_employee_number: cleanCedula,
p_photo_url: remoteStorageUrl
});
if (error) {
- console.warn(`[Supabase RPC Error] Cédula ${cedula}:`, error.message);
+ console.warn(`[Supabase RPC Error] Cédula ${cleanCedula}:`, error.message);
} else if (data && data.success === false) {
- console.log(`[Supabase Info] Cédula ${cedula}: ${data.message}`);
+ console.log(`[Supabase Info] Cédula ${cleanCedula}: ${data.message}`);
} else {
- console.log(`[Supabase Success] Foto sincronizada para la Cédula: ${cedula}`);
+ console.log(`[Supabase Success] Foto sincronizada para la Cédula: ${cleanCedula}`);
}
}
} catch (error) {
- console.error(`Sincronización abortada para la cédula ${cedula}:`, error);
+ console.error(`Sincronización abortada para la cédula ${cleanCedula}:`, error);
}
};
- // --- MAPEO DE COLUMNAS EXCEL CORREGIDO ---
+ // --- MAPEO DE COLUMNAS EXCEL CON LIMPIEZA DE CÉDULA ---
const handleExcelUpload = async (e) => {
const file = e.target.files[0];
if (!file) return;
@@ -111,7 +117,6 @@ export default function IdCardGenerator() {
.replace(/[\u0300-\u036f]/g, "");
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';
@@ -123,15 +128,17 @@ export default function IdCardGenerator() {
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: String(row.cedula || '').trim(),
+ employeeId: cleanedCedula,
fotoUrl: String(row.foto_url || '').trim()
};
- }).filter(emp => emp.name && emp.role);
+ }).filter(emp => emp.name && emp.role && emp.employeeId);
setBulkEmployees(formatted);
setBulkStatusText(`✅ ¡Cargados ${formatted.length} colaboradores con éxito!`);
@@ -156,7 +163,7 @@ export default function IdCardGenerator() {
onStateChange: setBulkDownloadStatus,
onProcessEmployeePhoto: handleBulkSupabasePhotoSync
});
- setBulkStatusText('🎉 ¡Lote guardado y sincronizado con Supabase!');
+ setBulkStatusText('🎉 ¡Lote guardado!');
} catch (error) {
console.error(error);
alert("Error procesando las imágenes remotas.");
@@ -202,7 +209,6 @@ export default function IdCardGenerator() {
-
{/* FORMULARIO MASIVO EXCEL */}
@@ -236,7 +242,6 @@ export default function IdCardGenerator() {
}}>
Formato de columnas requeridas en el Excel: nombre, puesto, cliente/proyecto, cedula, lenguaje, foto_url.
- Las imágenes se guardarán automáticamente en Supabase si no tienen una previa asignada.
)}
@@ -327,8 +332,13 @@ export default function IdCardGenerator() {
Código QR
-
- setEmployeeId(e.target.value)} />
+
+
-
>
);
diff --git a/src/script/script.jsx b/src/script/script.jsx
index 95d1c45..084fb02 100644
--- a/src/script/script.jsx
+++ b/src/script/script.jsx
@@ -44,27 +44,45 @@ export const getXLSXLib = () => loadScript('https://cdnjs.cloudflare.com/ajax/li
export function convertDriveUrlToDirect(url) {
if (!url) return '';
- const regExp = /(?:https?:\/\/)?(?:drive\.google\.com\/)(?:file\/d\/|open\?id=)([\w-]+)/;
- const match = url.match(regExp);
- if (match && match[1]) {
- return `https://docs.google.com/uc?export=download&id=${match[1]}`;
+
+ 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;
}
-// Descarga la imagen evadiendo las restricciones CORS de Google Drive usando un CDN/Proxy transparente
export async function fetchImageAsBlob(url) {
if (!url) 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 (e) {
+ console.warn("No se pudo obtener el Blob directo de Drive debido a restricciones CORS.");
+ }
+ 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);
+ console.error("Error haciendo fetch con bypass CORS a la imagen externa:", e);
try {
- // Intento secundario fallback por si falla el proxy
const response = await fetch(url, { mode: 'cors' });
return await response.blob();
} catch (err) {
@@ -79,19 +97,24 @@ function preloadImage(src) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(src);
- img.onerror = () => resolve(null);
+ img.onerror = () => {
+ const retryImg = new Image();
+ retryImg.onload = () => resolve(src);
+ retryImg.onerror = () => resolve(null);
+ retryImg.src = src;
+ };
img.src = src;
});
}
-// ── GENERADOR DE QR NATIVO ──
+// ── GENERADOR DE QR NATIVO (Garantiza ID limpio en URL) ──
export async function renderQRCode(employeeId, canvasElement) {
if (!canvasElement) return;
- const text = (employeeId || '').trim();
+ const cleanId = (employeeId || '').replace(/[-\s]/g, '');
const ctx = canvasElement.getContext('2d');
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
- if (text.length <= 2) return;
+ if (cleanId.length <= 2) return;
const QRCodeLib = await getQRLib();
const tmp = document.createElement('div');
@@ -99,7 +122,7 @@ export async function renderQRCode(employeeId, canvasElement) {
document.body.appendChild(tmp);
new QRCodeLib(tmp, {
- text: `http://localhost:5173/employee/${text}`,
+ text: `http://localhost:5173/employee/${cleanId}`,
width: 55,
height: 55,
colorDark: '#000000',
@@ -181,22 +204,19 @@ export async function downloadIDCards(config) {
vFront.style.cssText = cardStyleBase;
vFront.style.backgroundImage = `url('${frontBgUrl}')`;
- let photoStyleHtml = `background: #e1e9ee; display: flex; align-items: center; justify-content: center;`;
- let innerPhotoContent = ``;
+ let photoTagHtml = `
+