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
+15
-11
@@ -1,14 +1,18 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" type="image/jpeg"
|
||||||
<title>glm-card-generator</title>
|
href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_white_background.jpg" />
|
||||||
<script type="module" crossorigin src="/empleado-id/assets/index-CVcbyLxj.js"></script>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="stylesheet" crossorigin href="/empleado-id/assets/index-DZLLXZoS.css">
|
<title>glm-card-generator</title>
|
||||||
</head>
|
<script type="module" crossorigin src="/empleado-id/assets/index-EuHkvm43.js"></script>
|
||||||
<body>
|
<link rel="stylesheet" crossorigin href="/empleado-id/assets/index-DRjn2cG3.css">
|
||||||
<div id="root"></div>
|
</head>
|
||||||
</body>
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Vendored
BIN
Binary file not shown.
@@ -1,13 +1,17 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<head>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<link rel="icon" type="image/jpeg"
|
||||||
<title>glm-card-generator</title>
|
href="https://dbit.digitalcompass.agency/storage/v1/object/public/public-assets/GLM_white_background.jpg" />
|
||||||
</head>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<body>
|
<title>glm-card-generator</title>
|
||||||
<div id="root"></div>
|
</head>
|
||||||
<script type="module" src="/src/main.jsx"></script>
|
|
||||||
</body>
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -358,3 +358,266 @@ body {
|
|||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
border-color: #4F758B;
|
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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const friendlyError = (message) => {
|
||||||
|
const err = new Error(message);
|
||||||
|
err.friendly = true;
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
|
||||||
export default function IdCardGenerator() {
|
export default function IdCardGenerator() {
|
||||||
// --- Estados formulario individual ───
|
// --- Estados formulario individual ───
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
@@ -45,9 +51,19 @@ 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 [pendingPhotoFile, setPendingPhotoFile] = useState(null);
|
||||||
const [photoProcessing, setPhotoProcessing] = useState(false);
|
const [photoProcessing, setPhotoProcessing] = useState(false);
|
||||||
const [downloadStatus, setDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' });
|
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
|
// Bloquea el botón Descargar hasta que nombre, puesto e ID estén llenos
|
||||||
const isFormComplete = name.trim() && role.trim() && employeeId.trim();
|
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 [bulkDownloadStatus, setBulkDownloadStatus] = useState({ processing: false, text: '⬇ Descargar' });
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
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 fileInputRef = useRef(null);
|
||||||
const excelInputRef = useRef(null);
|
const excelInputRef = useRef(null);
|
||||||
const qrCanvasRef = useRef(null);
|
const qrCanvasRef = useRef(null);
|
||||||
@@ -71,26 +134,42 @@ export default function IdCardGenerator() {
|
|||||||
}
|
}
|
||||||
}, [employeeId]);
|
}, [employeeId]);
|
||||||
|
|
||||||
const handlePhotoUpload = async (e) => {
|
const processAndSetPhoto = async (file) => {
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
setPhotoFile(file);
|
|
||||||
setPhotoSrc(URL.createObjectURL(file));
|
|
||||||
setPhotoProcessing(true);
|
setPhotoProcessing(true);
|
||||||
|
setPendingPhotoFile(file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const editedBlob = await processPhotoWithAI(file);
|
const editedBlob = await processPhotoWithAI(file);
|
||||||
const editedUrl = URL.createObjectURL(editedBlob);
|
const editedUrl = URL.createObjectURL(editedBlob);
|
||||||
setPhotoSrc(editedUrl);
|
setPhotoSrc(editedUrl);
|
||||||
setPhotoFile(new File([editedBlob], file.name, { type: editedBlob.type || 'image/jpeg' }));
|
setPhotoFile(new File([editedBlob], file.name, { type: editedBlob.type || 'image/jpeg' }));
|
||||||
|
setPendingPhotoFile(null);
|
||||||
} catch (error) {
|
} 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 {
|
} finally {
|
||||||
setPhotoProcessing(false);
|
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 processPhotoWithAI = async (file) => {
|
||||||
const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL;
|
const N8N_WEBHOOK_URL = import.meta.env.VITE_N8N_WEBHOOK_URL;
|
||||||
const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN;
|
const TOKEN_SECRETO = import.meta.env.VITE_WEBHOOK_TOKEN;
|
||||||
@@ -111,7 +190,8 @@ export default function IdCardGenerator() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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') || '';
|
const contentType = response.headers.get('content-type') || '';
|
||||||
@@ -119,13 +199,13 @@ export default function IdCardGenerator() {
|
|||||||
const data = await response.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 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);
|
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();
|
const blob = await (await fetch(blobUrl)).blob();
|
||||||
return blob;
|
return blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await response.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;
|
return blob;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,7 +234,8 @@ export default function IdCardGenerator() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const resData = await response.json();
|
||||||
@@ -197,7 +278,7 @@ export default function IdCardGenerator() {
|
|||||||
const jsonRows = XLSXLib.utils.sheet_to_json(ws);
|
const jsonRows = XLSXLib.utils.sheet_to_json(ws);
|
||||||
|
|
||||||
if (jsonRows.length === 0) {
|
if (jsonRows.length === 0) {
|
||||||
setBulkStatusText('❌ El archivo está vacío.');
|
setBulkStatusText('❌ El archivo no tiene datos. Descarga la plantilla y agrega los colaboradores.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,12 +326,16 @@ export default function IdCardGenerator() {
|
|||||||
setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||||
|
|
||||||
} catch (innerErr) {
|
} 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);
|
reader.readAsBinaryString(file);
|
||||||
} catch (err) {
|
} 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([]);
|
setBulkEmployees([]);
|
||||||
|
|
||||||
} catch (error) {
|
} 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 {
|
} finally {
|
||||||
setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
setBulkDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||||
}
|
}
|
||||||
@@ -341,7 +428,8 @@ export default function IdCardGenerator() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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({
|
await downloadIDCards({
|
||||||
@@ -357,16 +445,20 @@ export default function IdCardGenerator() {
|
|||||||
await saveClienteProyecto(cleanCedula, selectedClient);
|
await saveClienteProyecto(cleanCedula, selectedClient);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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' });
|
setDownloadStatus({ processing: false, text: '⬇ Descargar' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
if (!name.trim()) { alert("Debe ingresar el nombre."); return false; }
|
if (!name.trim()) { alert("Escribe el nombre del colaborador para continuar."); return false; }
|
||||||
if (!role.trim()) { alert("Debe ingresar el puesto."); return false; }
|
if (!role.trim()) { alert("Escribe el puesto del colaborador para continuar."); return false; }
|
||||||
if (!employeeId.trim()) { alert("Debe ingresar el ID del empleado."); return false; }
|
if (!employeeId.trim()) { alert("Escribe el ID o cédula del colaborador para continuar."); return false; }
|
||||||
if (!photoFile) { alert("Debe subir la foto del colaborador."); return false; }
|
if (!photoFile) { alert("Sube la foto del colaborador para continuar."); return false; }
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -488,6 +580,24 @@ export default function IdCardGenerator() {
|
|||||||
{photoProcessing ? '⏳ Procesando foto con IA...' : '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} />
|
||||||
|
{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>
|
||||||
|
|
||||||
<div className="divider"></div>
|
<div className="divider"></div>
|
||||||
@@ -590,6 +700,74 @@ export default function IdCardGenerator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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) {
|
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);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user