feat: allow duplicated employee images in Supabase storage #3

Merged
EidanThen merged 2 commits from feature/n8n-integration into dev 2026-07-10 15:01:31 +00:00
3 changed files with 58 additions and 14 deletions
Showing only changes of commit cb0636c957 - Show all commits
+11 -5
View File
@@ -1,6 +1,6 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import './Generator.css'; import './Generator.css';
import { renderQRCode, downloadIDCards, downloadBulkIDCards, getXLSXLib } from './script/script.jsx'; import { renderQRCode, downloadIDCards, downloadBulkIDCards, getXLSXLib, clientTemplates } from './script/script.jsx';
import { uploadEmployeePhoto } from './services/storage.js'; import { uploadEmployeePhoto } from './services/storage.js';
import { supabase } from "./services/supabase"; import { supabase } from "./services/supabase";
@@ -148,11 +148,11 @@ export default function IdCardGenerator() {
}; };
// --- ALMACENAMIENTO AUTOMÁTICO EN EL STORAGE E INSTANCIACIÓN RPC --- // --- ALMACENAMIENTO AUTOMÁTICO EN EL STORAGE E INSTANCIACIÓN RPC ---
const handleBulkSupabasePhotoSync = async (cedula, blobData) => { const handleBulkSupabasePhotoSync = async (cedula, blobData, clientKey = '') => {
const cleanCedula = String(cedula).replace(/[-\s]/g, ''); const cleanCedula = String(cedula).replace(/[-\s]/g, '');
try { try {
const customFile = new File([blobData], `${cleanCedula}.jpg`, { type: 'image/jpeg' }); const customFile = new File([blobData], `${cleanCedula}.jpg`, { type: 'image/jpeg' });
const remoteStorageUrl = await uploadEmployeePhoto(cleanCedula, customFile); const remoteStorageUrl = await uploadEmployeePhoto(cleanCedula, customFile, clientKey);
if (remoteStorageUrl) { if (remoteStorageUrl) {
const { data, error } = await supabase.rpc("save_employee_photo", { const { data, error } = await supabase.rpc("save_employee_photo", {
@@ -354,7 +354,7 @@ export default function IdCardGenerator() {
if (!validateForm()) return; if (!validateForm()) return;
try { try {
if (photoFile) { if (photoFile) {
const photoUrl = await uploadEmployeePhoto(employeeId, photoFile); const photoUrl = await uploadEmployeePhoto(employeeId, photoFile, selectedClient);
await supabase.rpc("save_employee_photo", { await supabase.rpc("save_employee_photo", {
p_employee_number: employeeId, p_employee_number: employeeId,
p_photo_url: photoUrl p_photo_url: photoUrl
@@ -557,7 +557,13 @@ export default function IdCardGenerator() {
<div className="card-wrap"> <div className="card-wrap">
<div className="card-label">Reverso</div> <div className="card-label">Reverso</div>
<div className="card" id="cardBack" style={{ backgroundImage: `url('${baseUrl}${selectedClient === 'Colgate' ? 'images/AF Colgate' : 'images/AF ' + selectedClient} ${language}.png')` }}> <div
className="card"
id="cardBack"
style={{
backgroundImage: `url('${baseUrl}${(clientTemplates[language] || clientTemplates['Esp'])[selectedClient] || (clientTemplates[language] || clientTemplates['Esp'])['Generico']}')`
}}
>
<div className="back-qr-zone"> <div className="back-qr-zone">
<canvas ref={qrCanvasRef} width={55} height={55}></canvas> <canvas ref={qrCanvasRef} width={55} height={55}></canvas>
</div> </div>
+1 -1
View File
@@ -330,7 +330,7 @@ export async function downloadBulkIDCards({ employees, baseUrl, onStateChange, o
if (blobData) { if (blobData) {
validatedPhotoUrl = URL.createObjectURL(blobData); validatedPhotoUrl = URL.createObjectURL(blobData);
if (onProcessEmployeePhoto) { if (onProcessEmployeePhoto) {
await onProcessEmployeePhoto(strictCleanId, blobData); await onProcessEmployeePhoto(strictCleanId, blobData, emp.selectedClient || '');
} }
} }
} catch (err) { } catch (err) {
+46 -8
View File
@@ -1,21 +1,59 @@
import { supabase } from "./supabase"; import { supabase } from "./supabase";
export async function uploadEmployeePhoto(employeeNumber, file) { /**
* Sube la foto de un empleado al bucket "foto_empleados".
*
* El nombre del archivo sigue el patrón:
* <cedula>_<cliente>.jpg — primera foto
* <cedula>_<cliente>1.jpg — segunda foto del mismo empleado+cliente
* <cedula>_<cliente>2.jpg — tercera, etc.
*
* @param {string} employeeNumber - Número de cédula limpio (sin guiones/espacios)
* @param {File} file - Archivo de imagen a subir
* @param {string} [clientKey=''] - Nombre del cliente/proyecto (p.ej. "claro", "nestle")
* @returns {Promise<string>} - URL pública del archivo subido
*/
export async function uploadEmployeePhoto(employeeNumber, file, clientKey = '') {
const extension = file.name.split('.').pop() || 'jpg';
const extension = file.name.split(".").pop(); // Normalizar el nombre del cliente: minúsculas, sin espacios ni caracteres especiales
const safeClient = clientKey
? '_' + clientKey.toLowerCase().replace(/[^a-z0-9]/g, '')
: '';
const fileName = `${employeeNumber}.${extension}`; const baseName = `${employeeNumber}${safeClient}`;
// Buscar archivos existentes con el mismo prefijo para elegir el sufijo correcto
const { data: existingFiles } = await supabase.storage
.from('foto_empleados')
.list('', { search: baseName });
// Filtrar exactamente los archivos que tengan el mismo baseName base
const pattern = new RegExp(`^${baseName}(\\d*)\\.${extension}$`);
const matches = (existingFiles || []).filter(f => pattern.test(f.name));
let fileName;
if (matches.length === 0) {
// No existe ninguno: usar el nombre base sin sufijo numérico
fileName = `${baseName}.${extension}`;
} else {
// Extraer los sufijos numéricos usados y elegir el siguiente
const usedSuffixes = matches.map(f => {
const m = f.name.match(pattern);
return m ? parseInt(m[1] || '0', 10) : 0;
});
const nextSuffix = Math.max(...usedSuffixes) + 1;
fileName = `${baseName}${nextSuffix}.${extension}`;
}
const { error } = await supabase.storage const { error } = await supabase.storage
.from("foto_empleados") .from('foto_empleados')
.upload(fileName, file, { .upload(fileName, file, { upsert: false });
upsert: false
});
if (error) throw error; if (error) throw error;
const { data } = supabase.storage const { data } = supabase.storage
.from("foto_empleados") .from('foto_empleados')
.getPublicUrl(fileName); .getPublicUrl(fileName);
return data.publicUrl; return data.publicUrl;