feat: Separar paginas generador y qr en diferentes proyectos
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
VITE_SUPABASE_ANON_KEY=J5JS7HG...
|
||||
VITE_SUPABASE_URL=https://tudominio
|
||||
VITE_WEBHOOK_TOKEN=6d4g56d4fgd...
|
||||
VITE_N8N_WEBHOOK_URL=https://tudominio/webhook/carnet
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>glm-card-generator</title>
|
||||
<script type="module" crossorigin src="/empleado-id/assets/index-Ef45i7N1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/empleado-id/assets/index-OsvWWXE6.css">
|
||||
<script type="module" crossorigin src="/empleado-id/assets/index-hkdwba_S.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/empleado-id/assets/index-DZLLXZoS.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { BrowserRouter, Routes, Route, Navigate, useLocation } from "react-route
|
||||
import { supabase } from "./services/supabase";
|
||||
import { useEffect, useState, createContext, useContext } from "react";
|
||||
import Login from "./components/Login";
|
||||
import EmployeeCard from "./components/EmployeeCard";
|
||||
import IdCardGenerator from "./Generator";
|
||||
import "./components/Login.css";
|
||||
|
||||
@@ -157,16 +156,6 @@ export default function App() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/empleado-id/:employeeNumber"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<EmployeeCard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="*" element={<Navigate to="/empleado-id/" replace />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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";
|
||||
import { saveClienteProyecto } from './services/storage.js';
|
||||
|
||||
// Convierte datos binarios recibidos (base64, buffer, array de bytes) en un Blob URL local de forma robusta
|
||||
const convertBinaryToBlobUrl = (data) => {
|
||||
@@ -175,17 +174,6 @@ export default function IdCardGenerator() {
|
||||
// 1) Guardar cédula + cliente_proyecto en carnet_empleados_creados_glm
|
||||
await saveClienteProyecto(cleanCedula, clienteProyectoValue);
|
||||
|
||||
// 2) Subir foto al storage
|
||||
const customFile = new File([blobData], `${cleanCedula}.jpg`, { type: 'image/jpeg' });
|
||||
const remoteStorageUrl = await uploadEmployeePhoto(cleanCedula, customFile);
|
||||
|
||||
// 3) Guardar URL de foto
|
||||
if (remoteStorageUrl) {
|
||||
await supabase.rpc("save_employee_photo", {
|
||||
p_employee_number: cleanCedula,
|
||||
p_photo_url: remoteStorageUrl
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// silent
|
||||
}
|
||||
@@ -317,16 +305,43 @@ export default function IdCardGenerator() {
|
||||
|
||||
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
|
||||
});
|
||||
setDownloadStatus({ processing: true, text: '🔄 Descargando...' });
|
||||
|
||||
try {
|
||||
// Enviar binario (base64) + employeeId como JSON al webhook n8n
|
||||
const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL;
|
||||
const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN;
|
||||
|
||||
let photoBase64 = null;
|
||||
if (photoSrc && photoSrc.startsWith('blob:')) {
|
||||
const response = await fetch(photoSrc);
|
||||
if (response.ok) {
|
||||
const blob = await response.blob();
|
||||
photoBase64 = await new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result.split(',')[1]);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
data: photoBase64,
|
||||
employeeId: employeeId
|
||||
};
|
||||
|
||||
const response = await fetch(N8N_WEBHOOK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': TOKEN_SECRETO
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`);
|
||||
}
|
||||
|
||||
await downloadIDCards({
|
||||
@@ -335,6 +350,12 @@ export default function IdCardGenerator() {
|
||||
qrCanvas: qrCanvasRef.current,
|
||||
onStateChange: setDownloadStatus
|
||||
});
|
||||
|
||||
// Guardar cédula + cliente_proyecto en carnet_empleados_creados_glm al completar descarga
|
||||
const cleanCedula = String(employeeId || '').replace(/[-\s]/g, '');
|
||||
if (cleanCedula) {
|
||||
await saveClienteProyecto(cleanCedula, selectedClient);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
setDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||
@@ -342,214 +363,214 @@ export default function IdCardGenerator() {
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
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 (
|
||||
<>
|
||||
<div className="page-title">
|
||||
<h1>GLM ID Card Generator</h1>
|
||||
<p>GomezLee Marketing · Carnet Corporativo CR80</p>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="page-title">
|
||||
<h1>GLM ID Card Generator</h1>
|
||||
<p>GomezLee Marketing · Carnet Corporativo CR80</p>
|
||||
</div>
|
||||
|
||||
<div className="main-container" style={{ display: 'flex', gap: '20px', alignItems: 'flex-start' }}>
|
||||
{/* FORMULARIO MASIVO EXCEL */}
|
||||
<div className="panel" style={{ minWidth: '320px' }}>
|
||||
<div className="preview-badge" style={{ background: '#FFF3E0', color: '#E65100' }}>
|
||||
<div className="dot" style={{ background: '#E65100' }}></div>
|
||||
<span>Módulo de Lotes Automáticos</span>
|
||||
</div>
|
||||
<div className="main-container" style={{ display: 'flex', gap: '20px', alignItems: 'flex-start' }}>
|
||||
{/* FORMULARIO MASIVO EXCEL */}
|
||||
<div className="panel" style={{ minWidth: '320px' }}>
|
||||
<div className="preview-badge" style={{ background: '#FFF3E0', color: '#E65100' }}>
|
||||
<div className="dot" style={{ background: '#E65100' }}></div>
|
||||
<span>Módulo de Lotes Automáticos</span>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title" style={{ display: 'flex', alignItems: 'center', gap: '6px', position: 'relative' }}>
|
||||
<span>Carga Masiva de Colaboradores</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: '16px', height: '16px', borderRadius: '50%',
|
||||
background: '#cbd5e1', color: '#334155', fontSize: '11px',
|
||||
fontWeight: 'bold', cursor: 'help', userSelect: 'none'
|
||||
}}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
?
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title" style={{ display: 'flex', alignItems: 'center', gap: '6px', position: 'relative' }}>
|
||||
<span>Carga Masiva de Colaboradores</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
width: '16px', height: '16px', borderRadius: '50%',
|
||||
background: '#cbd5e1', color: '#334155', fontSize: '11px',
|
||||
fontWeight: 'bold', cursor: 'help', userSelect: 'none'
|
||||
}}
|
||||
onMouseEnter={() => setShowTooltip(true)}
|
||||
onMouseLeave={() => setShowTooltip(false)}
|
||||
>
|
||||
?
|
||||
</div>
|
||||
|
||||
{showTooltip && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '24px', left: '0', right: '0',
|
||||
background: '#1e293b', color: '#ffffff', padding: '10px',
|
||||
borderRadius: '6px', fontSize: '11px', lineHeight: '1.4',
|
||||
zIndex: '99', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
fontWeight: 'normal', textTransform: 'none'
|
||||
}}>
|
||||
Formato de columnas requeridas en el Excel: <br />
|
||||
<b style={{ color: '#6CC24A' }}>nombre, puesto, cliente/proyecto, cedula, lenguaje, foto_url</b>.<br />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showTooltip && (
|
||||
<div style={{
|
||||
position: 'absolute', top: '24px', left: '0', right: '0',
|
||||
background: '#1e293b', color: '#ffffff', padding: '10px',
|
||||
borderRadius: '6px', fontSize: '11px', lineHeight: '1.4',
|
||||
zIndex: '99', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)',
|
||||
fontWeight: 'normal', textTransform: 'none'
|
||||
}}>
|
||||
Formato de columnas requeridas en el Excel: <br />
|
||||
<b style={{ color: '#6CC24A' }}>nombre, puesto, cliente/proyecto, cedula, lenguaje, foto_url</b>.<br />
|
||||
<div className="upload-btn" style={{ background: '#475569', marginTop: '12px' }} onClick={() => excelInputRef.current.click()}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
Seleccionar Archivo Excel
|
||||
</div>
|
||||
<input type="file" accept=".xlsx, .xls, .csv" style={{ display: 'none' }} ref={excelInputRef} onChange={handleExcelUpload} />
|
||||
|
||||
{bulkStatusText && (
|
||||
<div style={{ fontSize: '12px', marginTop: '8px', fontWeight: '500', color: '#475569' }}>
|
||||
{bulkStatusText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="upload-btn" style={{ background: '#475569', marginTop: '12px' }} onClick={() => excelInputRef.current.click()}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<polyline points="14 2 14 8 20 8" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
</svg>
|
||||
Seleccionar Archivo Excel
|
||||
</div>
|
||||
<input type="file" accept=".xlsx, .xls, .csv" style={{ display: 'none' }} ref={excelInputRef} onChange={handleExcelUpload} />
|
||||
<div className="divider"></div>
|
||||
|
||||
{bulkStatusText && (
|
||||
<div style={{ fontSize: '12px', marginTop: '8px', fontWeight: '500', color: '#475569' }}>
|
||||
{bulkStatusText}
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Exportar Lote</div>
|
||||
<button
|
||||
className="export-btn blue"
|
||||
disabled={bulkDownloadStatus.processing || bulkEmployees.length === 0}
|
||||
onClick={triggerBulkDownload}
|
||||
style={{ opacity: bulkEmployees.length === 0 ? 0.6 : 1 }}
|
||||
>
|
||||
{bulkDownloadStatus.text}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FORMULARIO INDIVIDUAL */}
|
||||
<div className="panel">
|
||||
<div className="preview-badge">
|
||||
<div className="dot"></div>
|
||||
<span>Vista previa en tiempo real</span>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Colaborador</div>
|
||||
<div className="field">
|
||||
<label>Nombre</label>
|
||||
<input type="text" placeholder="Ej: Alexi Zabala" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Exportar Lote</div>
|
||||
<button
|
||||
className="export-btn blue"
|
||||
disabled={bulkDownloadStatus.processing || bulkEmployees.length === 0}
|
||||
onClick={triggerBulkDownload}
|
||||
style={{ opacity: bulkEmployees.length === 0 ? 0.6 : 1 }}
|
||||
>
|
||||
{bulkDownloadStatus.text}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FORMULARIO INDIVIDUAL */}
|
||||
<div className="panel">
|
||||
<div className="preview-badge">
|
||||
<div className="dot"></div>
|
||||
<span>Vista previa en tiempo real</span>
|
||||
</div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Colaborador</div>
|
||||
<div className="field">
|
||||
<label>Nombre</label>
|
||||
<input type="text" placeholder="Ej: Alexi Zabala" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Puesto</label>
|
||||
<input type="text" placeholder="Ej: Mercaderista" value={role} onChange={(e) => setRole(e.target.value)} />
|
||||
</div>
|
||||
<div className="upload-btn" onClick={() => !photoProcessing && fileInputRef.current.click()} style={{ opacity: photoProcessing ? 0.6 : 1, cursor: photoProcessing ? 'wait' : 'pointer' }}>
|
||||
{photoProcessing ? '⏳ Procesando foto con IA...' : 'Subir foto del colaborador'}
|
||||
</div>
|
||||
<input type="file" accept="image/*" style={{ display: 'none' }} ref={fileInputRef} onChange={handlePhotoUpload} />
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Cliente / Proyecto</div>
|
||||
<div className="lang-container">
|
||||
<button type="button" className={`lang-btn ${language === 'Esp' ? 'active' : ''}`} onClick={() => setLanguage('Esp')}>Español</button>
|
||||
<button type="button" className={`lang-btn ${language === 'Ing' ? 'active' : ''}`} onClick={() => setLanguage('Ing')}>Inglés</button>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Seleccionar Cliente</label>
|
||||
<select value={selectedClient} onChange={(e) => setSelectedClient(e.target.value)}>
|
||||
<option value="Generico">Genérico (Por defecto)</option>
|
||||
<option value="Claro">Claro</option>
|
||||
<option value="Colgate">Colgate</option>
|
||||
<option value="KitchenAid">KitchenAid</option>
|
||||
<option value="Kraft">Kraft</option>
|
||||
<option value="Motorola">Motorola</option>
|
||||
<option value="Nestle">Nestle</option>
|
||||
<option value="P&G">P&G</option>
|
||||
<option value="Philip Morris">Philip Morris International</option>
|
||||
<option value="Whirlpool">Whirlpool</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Código QR</div>
|
||||
<div className="field">
|
||||
<label>ID del empleado / Cédula</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: 123456789 (Sin guiones ni espacios)"
|
||||
value={employeeId}
|
||||
onChange={handleEmployeeIdChange}
|
||||
/>
|
||||
</div>
|
||||
<button className="export-btn" style={{ background: '#6CC24A', marginTop: '2px' }} onClick={() => renderQRCode(employeeId, qrCanvasRef.current)}>
|
||||
⟳ Generar QR
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Exportar</div>
|
||||
<button className="export-btn blue" disabled={downloadStatus.processing || !isFormComplete} onClick={triggerDownload} style={{ opacity: (!isFormComplete || downloadStatus.processing) ? 0.6 : 1 }}>
|
||||
{downloadStatus.text}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* VISTA PREVIA */}
|
||||
<div className="stage">
|
||||
<div className="card-wrap">
|
||||
<div className="card-label">Frente</div>
|
||||
<div className="card" id="cardFront" style={{ backgroundImage: `url('${baseUrl}images/AF GLM Frente.png')` }}>
|
||||
<div className="front-photo-frame" style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
{photoSrc ? (
|
||||
<img src={photoSrc} alt="Foto Colaborador" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
) : (null)}
|
||||
{photoProcessing && (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(0,0,0,0.45)', color: '#fff',
|
||||
fontSize: '13px', fontWeight: '600', textAlign: 'center', padding: '8px'
|
||||
}}>
|
||||
⏳ Editando foto con IA...
|
||||
</div>
|
||||
)}
|
||||
{!photoSrc && (
|
||||
<svg className="placeholder-svg" viewBox="0 0 28 28" fill="none" style={{ width: '60%', height: '60%', opacity: 0.4 }}>
|
||||
<circle cx="14" cy="10" r="5.5" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M3 24c0-6 22-6 22 0" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
)}
|
||||
<div className="field">
|
||||
<label>Puesto</label>
|
||||
<input type="text" placeholder="Ej: Mercaderista" value={role} onChange={(e) => setRole(e.target.value)} />
|
||||
</div>
|
||||
<div className="front-dynamic-name">{name ? name.toUpperCase() : 'NOMBRE APELLIDO'}</div>
|
||||
<div className="front-dynamic-role">{role ? role.toUpperCase() : 'PUESTO'}</div>
|
||||
<div className="upload-btn" onClick={() => !photoProcessing && fileInputRef.current.click()} style={{ opacity: photoProcessing ? 0.6 : 1, cursor: photoProcessing ? 'wait' : 'pointer' }}>
|
||||
{photoProcessing ? '⏳ Procesando foto con IA...' : 'Subir foto del colaborador'}
|
||||
</div>
|
||||
<input type="file" accept="image/*" style={{ display: 'none' }} ref={fileInputRef} onChange={handlePhotoUpload} />
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Cliente / Proyecto</div>
|
||||
<div className="lang-container">
|
||||
<button type="button" className={`lang-btn ${language === 'Esp' ? 'active' : ''}`} onClick={() => setLanguage('Esp')}>Español</button>
|
||||
<button type="button" className={`lang-btn ${language === 'Ing' ? 'active' : ''}`} onClick={() => setLanguage('Ing')}>Inglés</button>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>Seleccionar Cliente</label>
|
||||
<select value={selectedClient} onChange={(e) => setSelectedClient(e.target.value)}>
|
||||
<option value="Generico">Genérico (Por defecto)</option>
|
||||
<option value="Claro">Claro</option>
|
||||
<option value="Colgate">Colgate</option>
|
||||
<option value="KitchenAid">KitchenAid</option>
|
||||
<option value="Kraft">Kraft</option>
|
||||
<option value="Motorola">Motorola</option>
|
||||
<option value="Nestle">Nestle</option>
|
||||
<option value="P&G">P&G</option>
|
||||
<option value="Philip Morris">Philip Morris International</option>
|
||||
<option value="Whirlpool">Whirlpool</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Código QR</div>
|
||||
<div className="field">
|
||||
<label>ID del empleado / Cédula</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: 123456789 (Sin guiones ni espacios)"
|
||||
value={employeeId}
|
||||
onChange={handleEmployeeIdChange}
|
||||
/>
|
||||
</div>
|
||||
<button className="export-btn" style={{ background: '#6CC24A', marginTop: '2px' }} onClick={() => renderQRCode(employeeId, qrCanvasRef.current)}>
|
||||
⟳ Generar QR
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="panel-section">
|
||||
<div className="panel-section-title">Exportar</div>
|
||||
<button className="export-btn blue" disabled={downloadStatus.processing || !isFormComplete} onClick={triggerDownload} style={{ opacity: (!isFormComplete || downloadStatus.processing) ? 0.6 : 1 }}>
|
||||
{downloadStatus.text}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-wrap">
|
||||
<div className="card-label">Reverso</div>
|
||||
<div
|
||||
className="card"
|
||||
id="cardBack"
|
||||
style={{
|
||||
backgroundImage: `url('${baseUrl}${(clientTemplates[language] || clientTemplates['Esp'])[selectedClient] || (clientTemplates[language] || clientTemplates['Esp'])['Generico']}')`
|
||||
}}
|
||||
>
|
||||
<div className="back-qr-zone">
|
||||
<canvas ref={qrCanvasRef} width={55} height={55}></canvas>
|
||||
{/* VISTA PREVIA */}
|
||||
<div className="stage">
|
||||
<div className="card-wrap">
|
||||
<div className="card-label">Frente</div>
|
||||
<div className="card" id="cardFront" style={{ backgroundImage: `url('${baseUrl}images/AF GLM Frente.png')` }}>
|
||||
<div className="front-photo-frame" style={{ position: 'relative', overflow: 'hidden' }}>
|
||||
{photoSrc ? (
|
||||
<img src={photoSrc} alt="Foto Colaborador" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
|
||||
) : (null)}
|
||||
{photoProcessing && (
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0, display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(0,0,0,0.45)', color: '#fff',
|
||||
fontSize: '13px', fontWeight: '600', textAlign: 'center', padding: '8px'
|
||||
}}>
|
||||
⏳ Editando foto con IA...
|
||||
</div>
|
||||
)}
|
||||
{!photoSrc && (
|
||||
<svg className="placeholder-svg" viewBox="0 0 28 28" fill="none" style={{ width: '60%', height: '60%', opacity: 0.4 }}>
|
||||
<circle cx="14" cy="10" r="5.5" stroke="currentColor" strokeWidth="1.5" />
|
||||
<path d="M3 24c0-6 22-6 22 0" stroke="currentColor" strokeWidth="1.5" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<div className="front-dynamic-name">{name ? name.toUpperCase() : 'NOMBRE APELLIDO'}</div>
|
||||
<div className="front-dynamic-role">{role ? role.toUpperCase() : 'PUESTO'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card-wrap">
|
||||
<div className="card-label">Reverso</div>
|
||||
<div
|
||||
className="card"
|
||||
id="cardBack"
|
||||
style={{
|
||||
backgroundImage: `url('${baseUrl}${(clientTemplates[language] || clientTemplates['Esp'])[selectedClient] || (clientTemplates[language] || clientTemplates['Esp'])['Generico']}')`
|
||||
}}
|
||||
>
|
||||
<div className="back-qr-zone">
|
||||
<canvas ref={qrCanvasRef} width={55} height={55}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
|
||||
@@ -131,7 +131,7 @@ export async function renderQRCode(employeeId, canvasElement) {
|
||||
document.body.appendChild(tmp);
|
||||
|
||||
new QRCodeLib(tmp, {
|
||||
text: `http://digitalcompass.agency/empleado?id=${cleanId}`,
|
||||
text: `https://digitalcompass.agency/empleado?id=${cleanId}`,
|
||||
width: 55,
|
||||
height: 55,
|
||||
colorDark: '#000000',
|
||||
|
||||
Reference in New Issue
Block a user