feat: agregar boton de reporte
This commit is contained in:
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+11
-7
@@ -1,14 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/jpeg"
|
||||
href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_white_background.jpg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>glm-card-generator</title>
|
||||
<script type="module" crossorigin src="/empleado-id/assets/index-CVcbyLxj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/empleado-id/assets/index-DZLLXZoS.css">
|
||||
</head>
|
||||
<body>
|
||||
<script type="module" crossorigin src="/empleado-id/assets/index-EuHkvm43.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/empleado-id/assets/index-DRjn2cG3.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Vendored
BIN
Binary file not shown.
@@ -1,13 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/jpeg"
|
||||
href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_white_background.jpg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>glm-card-generator</title>
|
||||
</head>
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -358,3 +358,266 @@ body {
|
||||
color: #ffffff;
|
||||
border-color: #4F758B;
|
||||
}
|
||||
|
||||
/* ── REPORT FLOATING BUTTON ── */
|
||||
.report-fab {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background: #4F758B;
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 20px rgba(79, 117, 139, 0.4);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: width 0.35s ease, background 0.2s ease, box-shadow 0.2s ease;
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.report-fab:hover,
|
||||
.report-fab.attention {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.report-fab:hover {
|
||||
background: #3d5d6e;
|
||||
box-shadow: 0 6px 28px rgba(79, 117, 139, 0.5);
|
||||
}
|
||||
|
||||
.report-fab.attention {
|
||||
background: #E65100;
|
||||
box-shadow: 0 6px 28px rgba(230, 81, 0, 0.55);
|
||||
animation: fabPulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fabPulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.06); }
|
||||
}
|
||||
|
||||
.report-fab-label {
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
transition: max-width 0.35s ease, opacity 0.25s ease, margin-right 0.35s ease;
|
||||
}
|
||||
|
||||
.report-fab:hover .report-fab-label,
|
||||
.report-fab.attention .report-fab-label {
|
||||
max-width: 90px;
|
||||
opacity: 1;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.report-fab:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(79, 117, 139, 0.3);
|
||||
}
|
||||
|
||||
/* ── REPORT MODAL ── */
|
||||
.report-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
z-index: 1100;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.report-modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
animation: slideUp 0.25s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.report-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
}
|
||||
|
||||
.report-modal-header h3 {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #4F758B;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.report-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #6B8FA3;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.report-modal-close:hover {
|
||||
background: #F0F0F0;
|
||||
color: #4F758B;
|
||||
}
|
||||
|
||||
.report-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.report-modal-hint {
|
||||
font-size: 12px;
|
||||
color: #6B8FA3;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.report-modal-textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #E0E0E0;
|
||||
border-radius: 8px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 13px;
|
||||
color: #2a2a2a;
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
max-height: 300px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.report-modal-textarea:focus {
|
||||
border-color: #6CC24A;
|
||||
}
|
||||
|
||||
.report-modal-textarea::placeholder {
|
||||
color: #B0B0B0;
|
||||
}
|
||||
|
||||
.report-modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #E0E0E0;
|
||||
background: #FAFAFA;
|
||||
}
|
||||
|
||||
.report-modal-cancel,
|
||||
.report-modal-send {
|
||||
padding: 10px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.report-modal-cancel {
|
||||
background: #fff;
|
||||
border: 1px solid #D0D0D0;
|
||||
color: #4F758B;
|
||||
}
|
||||
|
||||
.report-modal-cancel:hover {
|
||||
background: #F0F0F0;
|
||||
border-color: #B0B0B0;
|
||||
}
|
||||
|
||||
.report-modal-send {
|
||||
background: #6CC24A;
|
||||
border: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.report-modal-send:hover:not(:disabled) {
|
||||
background: #5ab03f;
|
||||
}
|
||||
|
||||
.report-modal-send:disabled {
|
||||
background: #A0B2BC;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── REPORT MODAL SUCCESS STATE ── */
|
||||
.report-modal-success {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 14px;
|
||||
padding: 48px 24px;
|
||||
animation: successPop 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes successPop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.report-modal-success svg {
|
||||
filter: drop-shadow(0 4px 12px rgba(108, 194, 74, 0.35));
|
||||
}
|
||||
|
||||
.report-modal-success p {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #4F758B;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 480px) {
|
||||
.report-fab {
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.report-modal {
|
||||
margin: 12px;
|
||||
max-height: calc(100vh - 24px);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,12 @@ const convertBinaryToBlobUrl = (data) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const friendlyError = (message) => {
|
||||
const err = new Error(message);
|
||||
err.friendly = true;
|
||||
return err;
|
||||
};
|
||||
|
||||
export default function IdCardGenerator() {
|
||||
// --- Estados formulario individual ───
|
||||
const [name, setName] = useState('');
|
||||
@@ -45,9 +51,19 @@ export default function IdCardGenerator() {
|
||||
const [employeeId, setEmployeeId] = useState('');
|
||||
const [photoSrc, setPhotoSrc] = useState('');
|
||||
const [photoFile, setPhotoFile] = useState(null);
|
||||
const [pendingPhotoFile, setPendingPhotoFile] = useState(null);
|
||||
const [photoProcessing, setPhotoProcessing] = useState(false);
|
||||
const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' });
|
||||
|
||||
// --- Estados Reporte de Errores/Ayuda ───
|
||||
const [showReportModal, setShowReportModal] = useState(false);
|
||||
const [reportMessage, setReportMessage] = useState('');
|
||||
const [reportSending, setReportSending] = useState(false);
|
||||
const [reportSent, setReportSent] = useState(false);
|
||||
const reportCloseTimeoutRef = useRef(null);
|
||||
const [fabAttention, setFabAttention] = useState(false);
|
||||
const fabAttentionTimeoutRef = useRef(null);
|
||||
|
||||
// Bloquea el botón Descargar hasta que nombre, puesto e ID estén llenos
|
||||
const isFormComplete = name.trim() && role.trim() && employeeId.trim();
|
||||
|
||||
@@ -57,6 +73,53 @@ export default function IdCardGenerator() {
|
||||
const [bulkDownloadStatus, setBulkDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' });
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const handleSendReport = async () => {
|
||||
if (!reportMessage.trim() || reportSending) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReportSending(true);
|
||||
|
||||
try {
|
||||
const response = await fetch('https://agenteit.digitalcompass.agency/webhook/reporte-carnet', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
subject: 'Reporte - Generador de ID',
|
||||
body: reportMessage.trim(),
|
||||
fecha: new Date().toISOString()
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`El servidor rechazó la petición (Código: ${response.status})`);
|
||||
}
|
||||
|
||||
setReportMessage('');
|
||||
setReportSent(true);
|
||||
reportCloseTimeoutRef.current = setTimeout(() => {
|
||||
setShowReportModal(false);
|
||||
setReportSent(false);
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
console.error('Error al enviar el reporte:', error);
|
||||
triggerFabAttention();
|
||||
alert('❌ No pudimos enviar tu reporte. Revisa tu conexión e inténtalo de nuevo.');
|
||||
} finally {
|
||||
setReportSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerFabAttention = () => {
|
||||
setFabAttention(true);
|
||||
clearTimeout(fabAttentionTimeoutRef.current);
|
||||
fabAttentionTimeoutRef.current = setTimeout(() => {
|
||||
setFabAttention(false);
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const fileInputRef = useRef(null);
|
||||
const excelInputRef = useRef(null);
|
||||
const qrCanvasRef = useRef(null);
|
||||
@@ -71,26 +134,42 @@ export default function IdCardGenerator() {
|
||||
}
|
||||
}, [employeeId]);
|
||||
|
||||
const handlePhotoUpload = async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setPhotoFile(file);
|
||||
setPhotoSrc(URL.createObjectURL(file));
|
||||
const processAndSetPhoto = async (file) => {
|
||||
setPhotoProcessing(true);
|
||||
setPendingPhotoFile(file);
|
||||
|
||||
try {
|
||||
const editedBlob = await processPhotoWithAI(file);
|
||||
const editedUrl = URL.createObjectURL(editedBlob);
|
||||
setPhotoSrc(editedUrl);
|
||||
setPhotoFile(new File([editedBlob], file.name, { type: editedBlob.type || 'image/jpeg' }));
|
||||
setPendingPhotoFile(null);
|
||||
} catch (error) {
|
||||
alert('No se pudo procesar la foto con IA. Se usará la foto original para la vista previa.');
|
||||
console.error('Error al procesar la foto con IA:', error);
|
||||
triggerFabAttention();
|
||||
setPhotoSrc('');
|
||||
setPhotoFile(null);
|
||||
alert('No pudimos mejorar tu foto. Por favor intenta nuevamente; si sigue fallando, repórtanos el error con el botón de ayuda.');
|
||||
} finally {
|
||||
setPhotoProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePhotoUpload = (e) => {
|
||||
const file = e.target.files[0];
|
||||
e.target.value = '';
|
||||
if (!file || photoProcessing) return;
|
||||
|
||||
setPhotoSrc(URL.createObjectURL(file));
|
||||
processAndSetPhoto(file);
|
||||
};
|
||||
|
||||
const handleRetryPhotoAI = () => {
|
||||
if (!pendingPhotoFile || photoProcessing) return;
|
||||
setPhotoSrc(URL.createObjectURL(pendingPhotoFile));
|
||||
processAndSetPhoto(pendingPhotoFile);
|
||||
};
|
||||
|
||||
const processPhotoWithAI = async (file) => {
|
||||
const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL;
|
||||
const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN;
|
||||
@@ -111,7 +190,8 @@ export default function IdCardGenerator() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`);
|
||||
console.error('n8n rechazó la petición (foto IA). Status:', response.status);
|
||||
throw friendlyError('El servicio no está disponible en este momento. Espera un momento e inténtalo de nuevo.');
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
@@ -119,13 +199,13 @@ export default function IdCardGenerator() {
|
||||
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.');
|
||||
if (!blobUrl) throw friendlyError('La foto procesada llegó dañada. Intenta subirla de nuevo.');
|
||||
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.');
|
||||
if (!blob || blob.size === 0) throw friendlyError('La foto procesada llegó vacía. Intenta subirla de nuevo.');
|
||||
return blob;
|
||||
};
|
||||
|
||||
@@ -154,7 +234,8 @@ export default function IdCardGenerator() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`);
|
||||
console.error('n8n rechazó la petición (lote). Status:', response.status);
|
||||
throw friendlyError('El servicio no está disponible en este momento. Espera un momento e inténtalo de nuevo.');
|
||||
}
|
||||
|
||||
const resData = await response.json();
|
||||
@@ -197,7 +278,7 @@ export default function IdCardGenerator() {
|
||||
const jsonRows = XLSXLib.utils.sheet_to_json(ws);
|
||||
|
||||
if (jsonRows.length === 0) {
|
||||
setBulkStatusText('❌ El archivo está vacío.');
|
||||
setBulkStatusText('❌ El archivo no tiene datos. Descarga la plantilla y agrega los colaboradores.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -245,12 +326,16 @@ export default function IdCardGenerator() {
|
||||
setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||
|
||||
} catch (innerErr) {
|
||||
setBulkStatusText('❌ Error de lectura. Revisa el formato de columnas.');
|
||||
console.error('Error leyendo el Excel:', innerErr);
|
||||
triggerFabAttention();
|
||||
setBulkStatusText('❌ No pudimos leer el archivo. Descarga la plantilla oficial y verifica las columnas.');
|
||||
}
|
||||
};
|
||||
reader.readAsBinaryString(file);
|
||||
} catch (err) {
|
||||
setBulkStatusText('❌ Error cargando el motor XLSX.');
|
||||
console.error('Error cargando la librería XLSX:', err);
|
||||
triggerFabAttention();
|
||||
setBulkStatusText('❌ Tuvimos un problema técnico. Recarga la página e inténtalo de nuevo.');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -297,7 +382,9 @@ export default function IdCardGenerator() {
|
||||
setBulkEmployees([]);
|
||||
|
||||
} catch (error) {
|
||||
setBulkStatusText(`❌ Error al procesar: ${error.message || error}`);
|
||||
console.error('Error al procesar el lote:', error);
|
||||
triggerFabAttention();
|
||||
setBulkStatusText('❌ Algo salió mal al generar los carnets. Inténtalo de nuevo; si sigue fallando, avísanos con el botón de ayuda.');
|
||||
} finally {
|
||||
setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||
}
|
||||
@@ -341,7 +428,8 @@ export default function IdCardGenerator() {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`El servidor de n8n rechazó la petición (Código: ${response.status})`);
|
||||
console.error('n8n rechazó la petición (individual). Status:', response.status);
|
||||
throw friendlyError('El servicio no está disponible en este momento. Espera un momento e inténtalo de nuevo.');
|
||||
}
|
||||
|
||||
await downloadIDCards({
|
||||
@@ -357,16 +445,20 @@ export default function IdCardGenerator() {
|
||||
await saveClienteProyecto(cleanCedula, selectedClient);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(err.message);
|
||||
console.error('Error al generar el carnet:', err);
|
||||
triggerFabAttention();
|
||||
alert(err?.friendly
|
||||
? err.message
|
||||
: 'Algo salió mal al generar el carnet. Inténtalo de nuevo; si sigue fallando, avísanos con el botón de ayuda.');
|
||||
setDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||
}
|
||||
};
|
||||
|
||||
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; }
|
||||
if (!name.trim()) { alert("Escribe el nombre del colaborador para continuar."); return false; }
|
||||
if (!role.trim()) { alert("Escribe el puesto del colaborador para continuar."); return false; }
|
||||
if (!employeeId.trim()) { alert("Escribe el ID o cédula del colaborador para continuar."); return false; }
|
||||
if (!photoFile) { alert("Sube la foto del colaborador para continuar."); return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -488,6 +580,24 @@ export default function IdCardGenerator() {
|
||||
{photoProcessing ? '⏳ Procesando foto con IA...' : 'Subir foto del colaborador'}
|
||||
</div>
|
||||
<input type="file" accept="image/*" style={{ display: 'none' }} ref={fileInputRef} onChange={handlePhotoUpload} />
|
||||
{pendingPhotoFile && !photoProcessing && (
|
||||
<>
|
||||
<div style={{
|
||||
fontSize: '11px', fontWeight: '600', color: '#E65100',
|
||||
background: '#FFF3E0', border: '1px solid #FFCC80',
|
||||
borderRadius: '4px', padding: '7px 10px'
|
||||
}}>
|
||||
⚠️ La foto no pudo mejorarse. Inténtalo de nuevo.
|
||||
</div>
|
||||
<button
|
||||
className="export-btn"
|
||||
style={{ background: '#E65100' }}
|
||||
onClick={handleRetryPhotoAI}
|
||||
>
|
||||
⟳ Reintentar mejora de foto
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
@@ -590,6 +700,74 @@ export default function IdCardGenerator() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className={`report-fab ${fabAttention ? 'attention' : ''}`}
|
||||
onClick={() => {
|
||||
clearTimeout(fabAttentionTimeoutRef.current);
|
||||
setFabAttention(false);
|
||||
clearTimeout(reportCloseTimeoutRef.current);
|
||||
setReportMessage('');
|
||||
setReportSent(false);
|
||||
setShowReportModal(true);
|
||||
}}
|
||||
aria-label="Reportar error o pedir ayuda"
|
||||
title="Reportar error o pedir ayuda"
|
||||
>
|
||||
<span className="report-fab-label">Reportar</span>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{showReportModal && (
|
||||
<div className="report-modal-overlay" onClick={() => { if (!reportSending && !reportSent) setShowReportModal(false); }}>
|
||||
<div className="report-modal" onClick={(e) => e.stopPropagation()}>
|
||||
{reportSent ? (
|
||||
<div className="report-modal-success">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#6CC24A" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="8 12.5 11 15.5 16 9.5" />
|
||||
</svg>
|
||||
<p>Reporte enviado</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="report-modal-header">
|
||||
<h3>Reportar error / Pedir ayuda</h3>
|
||||
<button className="report-modal-close" onClick={() => { if (!reportSending) setShowReportModal(false); }} aria-label="Cerrar" disabled={reportSending}>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="report-modal-body">
|
||||
<p className="report-modal-hint">Describe el problema o tu solicitud:</p>
|
||||
<textarea
|
||||
className="report-modal-textarea"
|
||||
value={reportMessage}
|
||||
onChange={(e) => setReportMessage(e.target.value)}
|
||||
placeholder="Ej: La foto no se procesa correctamente, el QR no se genera, necesito agregar un cliente nuevo, etc."
|
||||
rows="5"
|
||||
disabled={reportSending}
|
||||
/>
|
||||
</div>
|
||||
<div className="report-modal-footer">
|
||||
<button className="report-modal-cancel" onClick={() => { setShowReportModal(false); setReportMessage(''); }} disabled={reportSending}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button className="report-modal-send" onClick={handleSendReport} disabled={!reportMessage.trim() || reportSending}>
|
||||
{reportSending ? '🔄 Enviando...' : 'Enviar'}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -36,7 +36,8 @@ export default function Login() {
|
||||
});
|
||||
|
||||
if (authError) {
|
||||
setError(authError.message);
|
||||
console.error('Error de autenticación:', authError);
|
||||
setError('No pudimos iniciar sesión. Verifica tu conexión e inténtalo de nuevo.');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user