Primer commit

This commit is contained in:
2026-06-30 12:24:04 -04:00
commit 510ebe8058
39 changed files with 4232 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
import React, { useState, useRef, useEffect } from 'react';
import './App.css';
// Importamos las funciones lógicas nativas desde el archivo portado script.jsx
import { renderQRCode, downloadIDCards } from './script/script.jsx';
import { uploadEmployeePhoto } from './services/storage.js';
import { supabase } from "./services/supabase";
export default function IdCardGenerator() {
// --- Estados para manejar la lógica en tiempo real ───
const [name, setName] = useState('');
const [role, setRole] = useState('');
const [language, setLanguage] = useState('Esp'); // 'Esp' o 'Ing'
const [selectedClient, setSelectedClient] = useState('Generico'); // Sin tildes de forma interna
const [employeeId, setEmployeeId] = useState('');
const [photoSrc, setPhotoSrc] = useState('');
const [photoFile, setPhotoFile] = useState(null);
// Estado para controlar la UI del botón de descarga mientras procesa
const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' });
// Referencias del DOM controladas por React
const fileInputRef = useRef(null);
const qrCanvasRef = useRef(null);
// EFECTO AUTOMÁTICO: Renderiza el QR en tiempo real mientras escribes mediante referencias
useEffect(() => {
if (qrCanvasRef.current) {
renderQRCode(employeeId, qrCanvasRef.current);
}
}, [employeeId]);
const handlePhotoUpload = (e) => {
const file = e.target.files[0];
if (file) {
setPhotoFile(file);
const localUrl = URL.createObjectURL(file);
setPhotoSrc(localUrl);
}
};
const handleLanguageChange = (lang) => {
setLanguage(lang);
};
const handleClientChange = (e) => {
setSelectedClient(e.target.value);
};
// Forzar generación del QR manualmente si se desea mediante el botón verde
const triggerGenerateQR = () => {
if (qrCanvasRef.current) {
renderQRCode(employeeId, qrCanvasRef.current);
}
};
// Disparador del motor de renderizado ULTRA MAX QUALITY en sandbox
const triggerDownload = async () => {
if (!validateForm()) {
return;
}
try {
// Solo si el usuario seleccionó una foto
if (photoFile) {
const photoUrl = await uploadEmployeePhoto(
employeeId,
photoFile
);
console.log("Foto subida:", photoUrl);
const { data, error } = await supabase.rpc(
"save_employee_photo",
{
p_employee_number: employeeId,
p_photo_url: photoUrl
}
);
if (error) {
throw error;
}
if (!data.success) {
console.warn(data.message);
} else {
console.log(data.message);
}
}
await downloadIDCards({
name,
role,
selectedClient,
language,
photoSrc,
baseUrl: import.meta.env.BASE_URL.endsWith('/')
? import.meta.env.BASE_URL
: `${import.meta.env.BASE_URL}/`,
qrCanvas: qrCanvasRef.current,
onStateChange: setDownloadStatus
});
} catch (err) {
console.error(err);
alert(err.message);
}
};
const baseUrl = import.meta.env.BASE_URL.endsWith('/')
? import.meta.env.BASE_URL
: `${import.meta.env.BASE_URL}/`;
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;
};
return (
<>
<div className="page-title">
<h1>GLM ID Card Generator</h1>
<p>GomezLee Marketing · Carnet Corporativo CR80</p>
</div>
<div className="main-container">
<div className="panel">
<div className="preview-badge">
<div className="dot"></div>
<span>Vista previa en tiempo real</span>
</div>
{/* Sección Colaborador */}
<div className="panel-section">
<div className="panel-section-title">Colaborador</div>
<div className="field">
<label>Nombre</label>
<input
type="text"
id="inputName"
placeholder="Ej: Alexi Zabala"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="field">
<label>Puesto</label>
<input
type="text"
id="inputRole"
placeholder="Ej: Mercaderista"
value={role}
onChange={(e) => setRole(e.target.value)}
/>
</div>
<div className="upload-btn" onClick={() => fileInputRef.current.click()}>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<circle cx="7" cy="5" r="3" stroke="currentColor" strokeWidth="1.2" />
<path d="M1 12c0-3.5 12-3.5 12 0" stroke="currentColor" strokeWidth="1.2" />
</svg>
Subir foto del colaborador
</div>
<input
type="file"
id="photoInput"
accept="image/*"
style={{ display: 'none' }}
ref={fileInputRef}
onChange={handlePhotoUpload}
/>
</div>
<div className="divider"></div>
{/* Sección Cliente / Proyecto */}
<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={() => handleLanguageChange('Esp')}
>
Español
</button>
<button
type="button"
className={`lang-btn ${language === 'Ing' ? 'active' : ''}`}
onClick={() => handleLanguageChange('Ing')}
>
Inglés
</button>
</div>
<div className="field">
<label>Seleccionar Cliente</label>
<select id="selectClient" value={selectedClient} onChange={handleClientChange}>
<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>
{/* Sección Código QR */}
<div className="panel-section">
<div className="panel-section-title">Código QR</div>
<div className="field">
<label>ID del empleado</label>
<input
type="text"
id="inputQR"
placeholder="Ej: 12345"
value={employeeId}
onChange={(e) => setEmployeeId(e.target.value)}
/>
</div>
<button
className="export-btn"
style={{ background: '#6CC24A', marginTop: '2px' }}
onClick={triggerGenerateQR}
>
Generar QR
</button>
</div>
<div className="divider"></div>
{/* Sección Exportar */}
<div className="panel-section">
<div className="panel-section-title">Exportar</div>
<button
className="export-btn blue"
id="btnDownload"
disabled={downloadStatus.processing}
onClick={triggerDownload}
>
{downloadStatus.text}
</button>
</div>
</div>
{/* --- STAGE: Vista previa de las tarjetas ─── */}
<div className="stage">
{/* Frente */}
<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" id="photoFrame" style={{ position: 'relative', overflow: 'hidden' }}>
{photoSrc ? (
<img
id="photoImg"
src={photoSrc}
alt="Foto Colaborador"
style={{
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block'
}}
/>
) : (
<svg className="placeholder-svg" id="photoPlaceholder" 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" id="displayName">
{name ? name.toUpperCase() : 'NOMBRE APELLIDO'}
</div>
<div className="front-dynamic-role" id="displayRole">
{role ? role.toUpperCase() : 'PUESTO'}
</div>
</div>
</div>
{/* Reverso */}
<div className="card-wrap">
<div className="card-label">Reverso</div>
<div
className="card"
id="cardBack"
style={{
backgroundImage: `url('${baseUrl}${selectedClient === 'Colgate' ? 'images/GLM Colgate' : 'images/AF ' + selectedClient} ${language}.png')`
}}
>
<div className="back-qr-zone">
{/* Asignamos la referencia de React para un control de pixeles seguro */}
<canvas ref={qrCanvasRef} width="55" height="55"></canvas>
</div>
</div>
</div>
</div>
</div>
</>
);
}