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
+359
View File
@@ -0,0 +1,359 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f0f2f5;
font-family: Arial, Helvetica, sans-serif;
min-height: 100vh;
padding: 28px 16px;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
/* ── PAGE TITLE ── */
.page-title {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.page-title h1 {
font-size: 13px;
font-weight: 700;
color: #4F758B;
letter-spacing: 3px;
text-transform: uppercase;
}
.page-title p {
font-size: 10px;
color: #6B8FA3;
letter-spacing: 1px;
}
/* ── MAIN CONTENT CONTAINER ── */
.main-container {
display: flex;
gap: 24px;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
flex-direction: row;
}
/* ── CONTROLS PANEL ── */
.panel {
background: #ffffff;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
padding: 20px;
width: 280px;
display: flex;
flex-direction: column;
gap: 16px;
order: 1;
}
/* ── CARD STAGE (Vertical Grid) ── */
.stage {
display: flex;
flex-direction: column;
gap: 20px;
align-items: flex-start;
flex-wrap: wrap;
justify-content: center;
order: 2;
}
@media(min-width: 768px) {
.stage {
flex-direction: row;
}
}
.panel-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.panel-section-title {
font-size: 9px;
font-weight: 700;
color: #4F758B;
letter-spacing: 2.5px;
text-transform: uppercase;
padding-bottom: 6px;
border-bottom: 1.5px solid #EEF6E8;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.field label {
font-size: 9px;
color: #6B8FA3;
letter-spacing: 1px;
text-transform: uppercase;
font-weight: 700;
}
.field input,
.field select {
background: #FAFAFA;
border: 1px solid #E0E0E0;
border-radius: 4px;
color: #2a2a2a;
font-family: Arial;
font-size: 12px;
padding: 7px 10px;
outline: none;
transition: border-color 0.15s;
}
.field input:focus,
.field select:focus {
border-color: #6CC24A;
background: #fff;
}
.field input::placeholder {
color: #C0C0C0;
}
.upload-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: #F5F5F5;
border: 1px dashed #D0D0D0;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
font-size: 11px;
color: #6B8FA3;
}
.upload-btn:hover {
border-color: #6CC24A;
color: #6CC24A;
background: #EEF6E8;
}
.divider {
height: 1px;
background: #F0F0F0;
}
.export-btn {
width: 100%;
padding: 11px;
background: #4F758B;
border: none;
border-radius: 5px;
color: #fff;
font-size: 11px;
font-family: Arial;
font-weight: 700;
cursor: pointer;
letter-spacing: 1.5px;
text-transform: uppercase;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.export-btn:hover {
background: #3d5d6e;
}
.export-btn.blue {
background: #4F758B;
}
.export-btn.blue:hover {
background: #385566;
}
.export-btn:disabled {
background: #A0B2BC;
cursor: not-allowed;
}
.hint-text {
font-size: 9px;
color: #B0B0B0;
line-height: 1.6;
text-align: center;
}
.preview-badge {
display: flex;
align-items: center;
gap: 6px;
background: #EEF6E8;
border-radius: 4px;
padding: 5px 10px;
}
.preview-badge .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #6CC24A;
}
.preview-badge span {
font-size: 9px;
color: #4F758B;
font-weight: 700;
letter-spacing: 1px;
}
/* CR80 Vertical Canvas Visual Container Setup */
.card-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.card-label {
font-size: 9px;
color: #6B8FA3;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: 700;
}
/* Dimensiones Proporcionales CR80 Verticales */
.card {
position: relative;
width: 240px;
height: 380px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
flex-shrink: 0;
}
/* ── FRONT DYNAMIC OVERLAYS ── */
.front-photo-frame {
position: absolute;
top: 99px;
left: 50%;
transform: translateX(-50%);
width: 145px;
height: 145px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.front-photo-frame img {
width: 100%;
height: 100%;
object-fit: cover;
display: none;
}
.front-photo-frame .placeholder-svg {
width: 40px;
height: 40px;
opacity: 0.3;
color: #4F758B;
}
.front-dynamic-name {
position: absolute;
top: 266px;
left: 10px;
right: 10px;
font-size: 14px;
font-weight: 900;
color: #FFFFFF;
text-align: center;
text-transform: uppercase;
letter-spacing: 0.5px;
word-break: break-word;
z-index: 5;
}
.front-dynamic-role {
position: absolute;
bottom: 12px;
right: 5px;
font-size: 10px;
font-weight: 700;
color: #6B8499;
text-align: right;
text-transform: uppercase;
letter-spacing: 1px;
max-width: 200px;
word-break: break-word;
z-index: 5;
}
/* ── BACK DYNAMIC OVERLAYS ── */
.back-qr-zone {
position: absolute;
bottom: 10px;
right: 10px;
background: #ffffff;
padding: 4px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
}
.back-qr-zone canvas {
display: block;
width: 40px;
height: 40px;
}
.lang-container {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.lang-btn {
flex: 1;
padding: 8px 12px;
font-size: 14px;
font-weight: bold;
border: 2px solid #e1e9ee;
background: #ffffff;
color: #4F758B;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.lang-btn.active {
background: #4F758B;
color: #ffffff;
border-color: #4F758B;
}
+29
View File
@@ -0,0 +1,29 @@
import { BrowserRouter, Routes, Route } from "react-router-dom";
import EmployeeCard from "./components/EmployeeCard";
import IdCardGenerator from "./Generator";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route
path="/employee"
element={<IdCardGenerator />}
/>
<Route
path="/employee/:employeeNumber"
element={<EmployeeCard />}
/>
</Routes>
</BrowserRouter>
);
}
+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>
</>
);
}
+67
View File
@@ -0,0 +1,67 @@
body {
margin: 0;
background: #f5f5f5;
font-family: Arial, sans-serif;
}
.page {
padding: 40px;
}
.employee-card {
width: 350px;
max-width: 100%;
margin: auto;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, .1);
}
.header {
color: white;
text-align: center;
padding: 20px;
font-size: 28px;
font-weight: bold;
}
.content {
padding: 30px;
text-align: center;
}
.label {
margin-top: 16px;
color: #666;
font-size: 14px;
}
.value {
font-size: 18px;
font-weight: bold;
}
.employee-photo {
width: 170px;
height: 170px;
border-radius: 50%;
object-fit: cover;
display: block;
margin: 0 auto 25px;
border: 4px solid #e5e5e5;
}
.employee-photo-placeholder {
width: 170px;
height: 170px;
border-radius: 50%;
margin: 0 auto 25px;
background: #f0f0f0;
color: #777;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
border: 4px solid #e5e5e5;
}
+127
View File
@@ -0,0 +1,127 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { supabase } from "../services/supabase";
import "./EmployeeCard.css";
export default function EmployeeCard() {
const { employeeNumber } = useParams();
const [employee, setEmployee] = useState(null);
const [loading, setLoading] = useState(true);
const [notFound, setNotFound] = useState(false);
useEffect(() => {
async function loadEmployee() {
const { data, error } = await supabase
.from("empleados_glm")
.select("photo_url, name, job_title, country, status")
.eq("cedula", employeeNumber)
.single();
if (error || !data) {
setNotFound(true);
} else {
setEmployee(data);
}
setLoading(false);
}
loadEmployee();
}, [employeeNumber]);
if (loading) {
return (
<div className="page">
<h2>Cargando...</h2>
</div>
);
}
if (notFound) {
return (
<div className="page">
<h1 style={{ color: "red" }}>
Empleado no encontrado
</h1>
</div>
);
}
const active = employee.status?.toLowerCase() === "active";
return (
<div className="page">
<div className="employee-card">
<div
className="header"
style={{
backgroundColor: active
? "#22c55e"
: "#ef4444"
}}
>
{active
? "Empleado Activo"
: "Empleado No Activo"}
</div>
<div className="content">
{employee.photo_url ? (
<img
src={employee.photo_url}
alt={employee.name}
className="employee-photo"
/>
) : (
<div className="employee-photo-placeholder">
Sin foto
</div>
)}
<div className="label">
Nombre
</div>
<div className="value">
{employee.name}
</div>
<div className="label">
Puesto
</div>
<div className="value">
{employee.job_title}
</div>
<div className="label">
País
</div>
<div className="value">
{employee.country}
</div>
<div className="label">
Estado
</div>
<div className="value">
{employee.status}
</div>
</div>
</div>
</div>
);
}
View File
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
+225
View File
@@ -0,0 +1,225 @@
// ── CONFIGURACIÓN DE PLANTILLAS DE CLIENTES ──
export const clientTemplates = {
'Esp': {
'Generico': 'images/AF Generico Esp.png',
'Claro': 'images/AF Claro Esp.png',
'Colgate': 'images/GLM Colgate Esp.png',
'KitchenAid': 'images/AF KitchenAid Esp.png',
'Kraft': 'images/AF Kraft Esp.png',
'Motorola': 'images/AF Motorola Esp.png',
'Nestle': 'images/AF Nestle Esp.png',
'P&G': 'images/AF P&G Esp.png',
'Philip Morris': 'images/AF Philip Morris Esp.png',
'Whirlpool': 'images/AF Whirlpool Esp.png'
},
'Ing': {
'Generico': 'images/AF Generico Ing.png',
'Claro': 'images/AF Claro Ing.png',
'Colgate': 'images/GLM Colgate Ing.png',
'KitchenAid': 'images/AF KitchenAid Ing.png',
'Kraft': 'images/AF Kraft Ing.png',
'Motorola': 'images/AF Motorola Ing.png',
'Nestle': 'images/AF Nestle Ing.png',
'P&G': 'images/AF P&G Ing.png',
'Philip Morris': 'images/AF Philip Morris Ing.png',
'Whirlpool': 'images/AF Whirlpool Ing.png'
}
};
// ── INYECTORES DINÁMICOS DE DEPENDENCIAS ──
function loadScript(src, globalKey) {
return new Promise((resolve) => {
if (window[globalKey]) return resolve(window[globalKey]);
const s = document.createElement('script');
s.src = src;
s.onload = () => resolve(window[globalKey]);
document.head.appendChild(s);
});
}
const getQRLib = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js', 'QRCode');
const getHtml2Canvas = () => loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js', 'html2canvas');
// ── GENERADOR DE QR NATIVO (REACT RENDERING) ──
export async function renderQRCode(employeeId, canvasElement) {
if (!canvasElement) return;
const text = (employeeId || '').trim();
const ctx = canvasElement.getContext('2d');
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
if (text.length <= 2) return;
const QRCodeLib = await getQRLib();
const tmp = document.createElement('div');
tmp.style.display = 'none';
document.body.appendChild(tmp);
new QRCodeLib(tmp, {
text: `http://localhost:5173/employee/${text}`,
width: 55,
height: 55,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: QRCodeLib.CorrectLevel.M
});
setTimeout(() => {
const img = tmp.querySelector('img') || tmp.querySelector('canvas');
const drawIt = (src) => {
const i = new Image();
i.onload = () => {
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 55, 55);
ctx.drawImage(i, 0, 0, 55, 55);
if (tmp.parentNode) document.body.removeChild(tmp);
};
i.src = src;
};
if (img && img.tagName === 'CANVAS') {
drawIt(img.toDataURL());
} else if (img) {
if (img.complete) drawIt(img.src);
else img.onload = () => drawIt(img.src);
} else {
if (tmp.parentNode) document.body.removeChild(tmp);
}
}, 50);
}
// ── ENGINE DE PROCESAMIENTO ULTRA MAX QUALITY (650x1004) ──
export async function downloadIDCards(config) {
const { name, role, selectedClient, language, photoSrc, baseUrl, qrCanvas, onStateChange } = config;
if (onStateChange) onStateChange({ processing: true, text: 'Procesando...' });
const html2canvasLib = await getHtml2Canvas();
const cleanName = name.trim().replace(/\s+/g, '') || 'Empleado';
// Resolver rutas absolutas
const frontBgUrl = `${baseUrl}images/AF GLM Frente.png`;
const currentGroup = clientTemplates[language] || clientTemplates['Esp'];
const backBgFile = currentGroup[selectedClient] || 'images/AF Generico Esp.png';
const backBgUrl = `${baseUrl}${backBgFile}`;
// Sandbox virtual aislado
const sandbox = document.createElement('div');
sandbox.style.position = 'fixed';
sandbox.style.top = '-9999px';
sandbox.style.left = '-9999px';
sandbox.style.width = '1400px';
document.body.appendChild(sandbox);
const cardStyleBase = `
position: relative;
width: 650px;
height: 1004px;
border-radius: 16px;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
display: inline-block;
margin-right: 50px;
overflow: hidden;
font-family: Arial, Helvetica, sans-serif;
`;
// 1. Frente Virtual en tamaño real
const vFront = document.createElement('div');
vFront.style.cssText = cardStyleBase;
vFront.style.backgroundImage = `url('${frontBgUrl}')`;
let photoStyleHtml = `background: #e1e9ee; display: flex; align-items: center; justify-content: center;`;
let innerPhotoContent = `<svg style="width:100px; height:100px; opacity:0.3; color:#4F758B;" viewBox="0 0 28 28" fill="none"><circle cx="14" cy="10" r="5.5" stroke="currentColor" stroke-width="1.5"/><path d="M3 24c0-6 22-6 22 0" stroke="currentColor" stroke-width="1.5"/></svg>`;
if (photoSrc) {
photoStyleHtml = `
background-image: url('${photoSrc}') !important;
background-size: cover !important;
background-position: center center !important;
background-repeat: no-repeat !important;
`;
innerPhotoContent = '';
}
vFront.innerHTML = `
<div style="position:absolute; top:260.5px; left:131.5px; width:384px; height:384px; border-radius:50%; overflow:hidden; z-index:5; ${photoStyleHtml}">
${innerPhotoContent}
</div>
<div style="position:absolute; top:715px; left:20px; right:20px; font-size:38px; font-weight:900; color:#FFFFFF; text-align:center; text-transform:uppercase; letter-spacing:1px; word-break:break-word; z-index:5;">
${name.toUpperCase() || 'NOMBRE APELLIDO'}
</div>
<div style="position:absolute; bottom:32px; right:15px; font-size:26px; font-weight:700; color:#6B8499; text-align:right; text-transform:uppercase; letter-spacing:1px; max-width:550px; white-space:nowrap; z-index:5;">
${role.toUpperCase() || 'PUESTO'}
</div>
`;
// 2. Reverso Virtual en tamaño real
const vBack = document.createElement('div');
vBack.style.cssText = cardStyleBase;
vBack.style.backgroundImage = `url('${backBgUrl}')`;
const vQRZone = document.createElement('div');
vQRZone.style.cssText = `
position: absolute;
bottom: 27px;
right: 27px;
background: #ffffff;
padding: 11px;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
`;
const vQRCanvas = document.createElement('canvas');
vQRCanvas.width = 149;
vQRCanvas.height = 149;
vQRCanvas.style.width = '100px';
vQRCanvas.style.height = '100px';
const vQRContext = vQRCanvas.getContext('2d');
if (vQRContext && qrCanvas) {
vQRContext.imageSmoothingEnabled = false;
vQRContext.drawImage(qrCanvas, 0, 0, 149, 149);
}
vQRZone.appendChild(vQRCanvas);
vBack.appendChild(vQRZone);
sandbox.appendChild(vFront);
sandbox.appendChild(vBack);
const renderOpts = {
scale: 1,
useCORS: true,
allowTaint: false,
backgroundColor: null,
logging: false,
width: 650,
height: 1004
};
try {
const frontCanvas = await html2canvasLib(vFront, renderOpts);
executeFileDownload(frontCanvas.toDataURL('image/png'), `${cleanName}Frente.png`);
const backCanvas = await html2canvasLib(vBack, renderOpts);
executeFileDownload(backCanvas.toDataURL('image/png'), `${cleanName}Reverso.png`);
} catch (error) {
console.error('Error generando las vistas en alta resolución:', error);
} finally {
if (sandbox.parentNode) document.body.removeChild(sandbox);
if (onStateChange) onStateChange({ processing: false, text: '⬇ Descargar' });
}
}
function executeFileDownload(dataUrl, fileName) {
const link = document.createElement('a');
link.href = dataUrl;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
+13
View File
@@ -0,0 +1,13 @@
import { supabase } from "./supabase";
export async function getEmployee(employeeNumber) {
const { data, error } = await supabase
.from("empleados_glm")
.select("*")
.eq("cedula", employeeNumber)
.single();
if (error) throw error;
return data;
}
+22
View File
@@ -0,0 +1,22 @@
import { supabase } from "./supabase";
export async function uploadEmployeePhoto(employeeNumber, file) {
const extension = file.name.split(".").pop();
const fileName = `${employeeNumber}.${extension}`;
const { error } = await supabase.storage
.from("foto_empleados")
.upload(fileName, file, {
upsert: false
});
if (error) throw error;
const { data } = supabase.storage
.from("foto_empleados")
.getPublicUrl(fileName);
return data.publicUrl;
}
+6
View File
@@ -0,0 +1,6 @@
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_SERVICE_ROLE_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);