feat: implement bulk ID card generation via Excel upload and individual form processing with Supabase storage integration #6
+1
-1
@@ -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
@@ -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" />
|
||||||
|
|||||||
@@ -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 = `
|
||||||
|
|||||||
Reference in New Issue
Block a user