2 Commits

3 changed files with 79 additions and 13 deletions
+1 -1
View File
@@ -273,7 +273,7 @@ body {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: none; display: block;
} }
.front-photo-frame .placeholder-svg { .front-photo-frame .placeholder-svg {
+76 -10
View File
@@ -46,8 +46,12 @@ export default function IdCardGenerator() {
const [employeeId, setEmployeeId] = useState(''); const [employeeId, setEmployeeId] = useState('');
const [photoSrc, setPhotoSrc] = useState(''); const [photoSrc, setPhotoSrc] = useState('');
const [photoFile, setPhotoFile] = useState(null); const [photoFile, setPhotoFile] = useState(null);
const [photoProcessing, setPhotoProcessing] = useState(false);
const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' }); const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' });
// Bloquea el botón Descargar hasta que nombre, puesto e ID estén llenos
const isFormComplete = name.trim() && role.trim() && employeeId.trim();
// --- Estados Lote / Excel Masivo ─── // --- Estados Lote / Excel Masivo ───
const [bulkEmployees, setBulkEmployees] = useState([]); const [bulkEmployees, setBulkEmployees] = useState([]);
const [bulkStatusText, setBulkStatusText] = useState(''); const [bulkStatusText, setBulkStatusText] = useState('');
@@ -68,14 +72,65 @@ export default function IdCardGenerator() {
} }
}, [employeeId]); }, [employeeId]);
const handlePhotoUpload = (e) => { const handlePhotoUpload = async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (!file) return;
setPhotoFile(file);
setPhotoSrc(URL.createObjectURL(file)); setPhotoFile(file);
setPhotoSrc(URL.createObjectURL(file));
setPhotoProcessing(true);
try {
const editedBlob = await processPhotoWithAI(file);
const editedUrl = URL.createObjectURL(editedBlob);
setPhotoSrc(editedUrl);
setPhotoFile(new File([editedBlob], file.name, { type: editedBlob.type || 'image/jpeg' }));
} catch (error) {
console.error('Error al procesar la foto con IA en n8n:', error);
alert('No se pudo procesar la foto con IA. Se usará la foto original para la vista previa.');
} finally {
setPhotoProcessing(false);
} }
}; };
const processPhotoWithAI = async (file) => {
const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL;
const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN;
const formData = new FormData();
formData.append('edited', file, file.name);
formData.append('name', name);
formData.append('role', role);
formData.append('employeeId', employeeId);
formData.append('fechaProcesado', new Date().toISOString());
const response = await fetch(N8N_WEBHOOK_URL, {
method: 'POST',
headers: {
'Authorization': TOKEN_SECRETO
},
body: formData
});
if (!response.ok) {
throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`);
}
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
const data = await response.json();
const bin = data.edited || data.data || data.foto_processed_base64 || data.photo || data.binary || (Array.isArray(data) && (data[0]?.edited || data[0]?.data));
const blobUrl = convertBinaryToBlobUrl(bin);
if (!blobUrl) throw new Error('n8n no devolvió un binario válido.');
const blob = await (await fetch(blobUrl)).blob();
return blob;
}
const blob = await response.blob();
if (!blob || blob.size === 0) throw new Error('n8n devolvió un binario vacío.');
return blob;
};
const handleEmployeeIdChange = (e) => { const handleEmployeeIdChange = (e) => {
const rawValue = e.target.value; const rawValue = e.target.value;
const cleanValue = rawValue.replace(/[-\s]/g, ''); const cleanValue = rawValue.replace(/[-\s]/g, '');
@@ -217,7 +272,7 @@ export default function IdCardGenerator() {
if (bulkEmployees.length === 0) return; if (bulkEmployees.length === 0) return;
setBulkDownloadStatus({ processing: true, text: '🔄 Descargando...' }); setBulkDownloadStatus({ processing: true, text: '🔄 Descargando...' });
setBulkStatusText('Conectando con n8n y descargando retratos optimizados por IA...'); setBulkStatusText('Descargando retratos...');
try { try {
const resultadosN8n = await enviarDatosAn8n(bulkEmployees); const resultadosN8n = await enviarDatosAn8n(bulkEmployees);
@@ -253,7 +308,7 @@ export default function IdCardGenerator() {
} }
}); });
setBulkStatusText('🎉 ¡Lote de carnets guardado correctamente y registrado en Supabase!'); setBulkStatusText('🎉 ¡Lote de carnets guardado correctamente!');
setBulkEmployees([]); setBulkEmployees([]);
} catch (error) { } catch (error) {
@@ -394,8 +449,8 @@ export default function IdCardGenerator() {
<label>Puesto</label> <label>Puesto</label>
<input type="text" placeholder="Ej: Mercaderista" value={role} onChange={(e) => setRole(e.target.value)} /> <input type="text" placeholder="Ej: Mercaderista" value={role} onChange={(e) => setRole(e.target.value)} />
</div> </div>
<div className="upload-btn" onClick={() => fileInputRef.current.click()}> <div className="upload-btn" onClick={() => !photoProcessing && fileInputRef.current.click()} style={{ opacity: photoProcessing ? 0.6 : 1, cursor: photoProcessing ? 'wait' : 'pointer' }}>
Subir foto del colaborador {photoProcessing ? '⏳ Procesando foto con IA...' : 'Subir foto del colaborador'}
</div> </div>
<input type="file" accept="image/*" style={{ display: 'none' }} ref={fileInputRef} onChange={handlePhotoUpload} /> <input type="file" accept="image/*" style={{ display: 'none' }} ref={fileInputRef} onChange={handlePhotoUpload} />
</div> </div>
@@ -447,7 +502,7 @@ export default function IdCardGenerator() {
<div className="panel-section"> <div className="panel-section">
<div className="panel-section-title">Exportar</div> <div className="panel-section-title">Exportar</div>
<button className="export-btn blue" disabled={downloadStatus.processing} onClick={triggerDownload}> <button className="export-btn blue" disabled={downloadStatus.processing || !isFormComplete} onClick={triggerDownload} style={{ opacity: (!isFormComplete || downloadStatus.processing) ? 0.6 : 1 }}>
{downloadStatus.text} {downloadStatus.text}
</button> </button>
</div> </div>
@@ -461,7 +516,18 @@ export default function IdCardGenerator() {
<div className="front-photo-frame" style={{ position: 'relative', overflow: 'hidden' }}> <div className="front-photo-frame" style={{ position: 'relative', overflow: 'hidden' }}>
{photoSrc ? ( {photoSrc ? (
<img src={photoSrc} alt="Foto Colaborador" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> <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 }}> <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" /> <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" /> <path d="M3 24c0-6 22-6 22 0" stroke="currentColor" strokeWidth="1.5" />
+2 -2
View File
@@ -221,7 +221,7 @@ export async function downloadIDCards(config) {
`; `;
if (photoSrc) { if (photoSrc) {
photoTagHtml = `<img src="${photoSrc}" crossorigin="anonymous" style="width:100%; height:100%; object-fit:cover; display:block;" />`; photoTagHtml = `<div style="width:100%; height:100%; background-image:url('${photoSrc}'); background-size:cover; background-position:center; background-repeat:no-repeat;"></div>`;
} }
vFront.innerHTML = ` vFront.innerHTML = `
@@ -356,7 +356,7 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange, o
`; `;
if (validatedPhotoUrl) { if (validatedPhotoUrl) {
photoTagHtml = `<img src="${validatedPhotoUrl}" crossorigin="anonymous" style="width:100%; height:100%; object-fit:cover; display:block;" />`; photoTagHtml = `<div style="width:100%; height:100%; background-image:url('${validatedPhotoUrl}'); background-size:cover; background-position:center; background-repeat:no-repeat;"></div>`;
} }
vFront.innerHTML = ` vFront.innerHTML = `