335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
|
|
import React, { useEffect, useRef, useState } from "react";
|
||
|
|
|
||
|
|
interface StimulsoftViewerProps {
|
||
|
|
requestUrl: string;
|
||
|
|
action?: string;
|
||
|
|
width?: string;
|
||
|
|
height?: string;
|
||
|
|
onClose?: () => void;
|
||
|
|
orderId?: number;
|
||
|
|
model?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* StimulsoftViewer Component
|
||
|
|
* Componente para renderizar o Stimulsoft Reports Viewer
|
||
|
|
* Baseado no componente stimulsoft-viewer-angular do portal Angular
|
||
|
|
*
|
||
|
|
* O componente usa a biblioteca Stimulsoft Reports.JS para processar o JSON
|
||
|
|
* retornado pelo servidor e renderizar o viewer
|
||
|
|
*/
|
||
|
|
const StimulsoftViewer: React.FC<StimulsoftViewerProps> = ({
|
||
|
|
requestUrl,
|
||
|
|
action,
|
||
|
|
width = "100%",
|
||
|
|
height = "800px",
|
||
|
|
onClose,
|
||
|
|
orderId,
|
||
|
|
model,
|
||
|
|
}) => {
|
||
|
|
const viewerRef = useRef<HTMLDivElement>(null);
|
||
|
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
||
|
|
const formRef = useRef<HTMLFormElement | null>(null);
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
// Gerar ID único para o viewer (deve ser declarado antes do useEffect)
|
||
|
|
const viewerIdRef = useRef(
|
||
|
|
`stimulsoft-viewer-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||
|
|
);
|
||
|
|
const viewerId = viewerIdRef.current;
|
||
|
|
|
||
|
|
// Construir a URL base (sem query params, pois vamos usar POST)
|
||
|
|
const baseUrl = action ? requestUrl.replace("{action}", action) : requestUrl;
|
||
|
|
const urlWithoutParams = baseUrl.split("?")[0];
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
let isMounted = true;
|
||
|
|
|
||
|
|
if (!viewerRef.current) return;
|
||
|
|
|
||
|
|
// Limpar conteúdo anterior
|
||
|
|
viewerRef.current.innerHTML = "";
|
||
|
|
|
||
|
|
// Extrair parâmetros da URL original se não foram fornecidos via props
|
||
|
|
const urlParams = new URLSearchParams(baseUrl.split("?")[1] || "");
|
||
|
|
const finalOrderId = orderId
|
||
|
|
? String(orderId)
|
||
|
|
: urlParams.get("orderId") || "";
|
||
|
|
const finalModel = model || urlParams.get("model") || "A";
|
||
|
|
|
||
|
|
// Criar iframe
|
||
|
|
const iframe = document.createElement("iframe");
|
||
|
|
iframe.name = `stimulsoft-viewer-iframe-${Date.now()}`;
|
||
|
|
iframe.style.width = "100%";
|
||
|
|
iframe.style.height = "100%";
|
||
|
|
iframe.style.border = "none";
|
||
|
|
iframe.style.borderRadius = "0 0 1.5rem 1.5rem";
|
||
|
|
iframe.title = "Stimulsoft Viewer";
|
||
|
|
iframe.allow = "fullscreen";
|
||
|
|
|
||
|
|
// Criar formulário que faz POST com multipart/form-data
|
||
|
|
// O componente Angular stimulsoft-viewer-angular faz POST com esses parâmetros
|
||
|
|
// O navegador vai criar o boundary automaticamente quando usamos multipart/form-data
|
||
|
|
const form = document.createElement("form");
|
||
|
|
form.method = "POST";
|
||
|
|
form.action = urlWithoutParams;
|
||
|
|
form.target = iframe.name;
|
||
|
|
form.setAttribute("enctype", "multipart/form-data");
|
||
|
|
form.style.display = "none";
|
||
|
|
|
||
|
|
// Adicionar campos do formulário conforme a requisição funcional do Angular
|
||
|
|
const fields = {
|
||
|
|
orderId: finalOrderId,
|
||
|
|
model: finalModel,
|
||
|
|
stiweb_component: "Viewer",
|
||
|
|
stiweb_imagesScalingFactor: "1",
|
||
|
|
stiweb_action: "AngularViewerData",
|
||
|
|
};
|
||
|
|
|
||
|
|
// Log para debug
|
||
|
|
console.log("StimulsoftViewer - URL:", urlWithoutParams);
|
||
|
|
console.log("StimulsoftViewer - Campos:", fields);
|
||
|
|
|
||
|
|
Object.entries(fields).forEach(([key, value]) => {
|
||
|
|
const input = document.createElement("input");
|
||
|
|
input.type = "hidden";
|
||
|
|
input.name = key;
|
||
|
|
input.value = String(value);
|
||
|
|
form.appendChild(input);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Verificar se orderId está vazio (pode causar erro 500)
|
||
|
|
if (!finalOrderId) {
|
||
|
|
setError("Erro: orderId não foi fornecido.");
|
||
|
|
setLoading(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let loadTimeout: NodeJS.Timeout;
|
||
|
|
let errorDetected = false;
|
||
|
|
|
||
|
|
const handleIframeLoad = () => {
|
||
|
|
if (isMounted && !errorDetected) {
|
||
|
|
// Verificar se houve erro no iframe (status 500, etc)
|
||
|
|
try {
|
||
|
|
const iframeDoc =
|
||
|
|
iframe.contentDocument || iframe.contentWindow?.document;
|
||
|
|
if (iframeDoc) {
|
||
|
|
const bodyText =
|
||
|
|
iframeDoc.body?.innerText || iframeDoc.body?.textContent || "";
|
||
|
|
// Se o conteúdo contém indicadores de erro
|
||
|
|
if (
|
||
|
|
bodyText.includes("500") ||
|
||
|
|
bodyText.includes("Internal Server Error") ||
|
||
|
|
bodyText.includes("Error")
|
||
|
|
) {
|
||
|
|
errorDetected = true;
|
||
|
|
setError(
|
||
|
|
"Erro ao carregar o relatório. O servidor retornou um erro."
|
||
|
|
);
|
||
|
|
setLoading(false);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
// CORS pode impedir acesso ao conteúdo do iframe, mas isso é normal
|
||
|
|
console.log(
|
||
|
|
"Não foi possível verificar conteúdo do iframe (pode ser CORS):",
|
||
|
|
e
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
clearTimeout(loadTimeout);
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleIframeError = () => {
|
||
|
|
if (isMounted) {
|
||
|
|
errorDetected = true;
|
||
|
|
setError(
|
||
|
|
"Erro ao carregar o relatório. Verifique a URL e a conexão com o servidor."
|
||
|
|
);
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Timeout para detectar se o iframe não carregou
|
||
|
|
loadTimeout = setTimeout(() => {
|
||
|
|
if (isMounted && loading) {
|
||
|
|
// Verificar se o iframe realmente carregou
|
||
|
|
try {
|
||
|
|
if (iframe.contentWindow && iframe.contentDocument) {
|
||
|
|
// Iframe carregou, mas pode ter demorado
|
||
|
|
handleIframeLoad();
|
||
|
|
} else {
|
||
|
|
errorDetected = true;
|
||
|
|
setError("Timeout ao carregar o relatório. Verifique a conexão.");
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
// CORS pode impedir acesso, mas isso não significa erro
|
||
|
|
// Se passou muito tempo, pode ser um problema
|
||
|
|
errorDetected = true;
|
||
|
|
setError("Timeout ao carregar o relatório.");
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, 30000); // 30 segundos de timeout
|
||
|
|
|
||
|
|
iframe.addEventListener("load", handleIframeLoad);
|
||
|
|
iframe.addEventListener("error", handleIframeError);
|
||
|
|
|
||
|
|
// Adicionar iframe primeiro ao DOM e aguardar estar pronto
|
||
|
|
if (viewerRef.current) {
|
||
|
|
viewerRef.current.appendChild(iframe);
|
||
|
|
}
|
||
|
|
|
||
|
|
formRef.current = form;
|
||
|
|
iframeRef.current = iframe;
|
||
|
|
|
||
|
|
// Aguardar o iframe estar completamente carregado antes de submeter o formulário
|
||
|
|
const waitForIframeReady = () => {
|
||
|
|
if (!isMounted) return;
|
||
|
|
|
||
|
|
// Verificar se o iframe está pronto
|
||
|
|
try {
|
||
|
|
// Tentar acessar o contentWindow para verificar se está pronto
|
||
|
|
if (iframe.contentWindow) {
|
||
|
|
// Iframe está pronto, adicionar formulário e submeter
|
||
|
|
// Adicionar formulário ao body do documento
|
||
|
|
document.body.appendChild(form);
|
||
|
|
|
||
|
|
// Aguardar um pequeno delay antes de submeter
|
||
|
|
setTimeout(() => {
|
||
|
|
if (isMounted && formRef.current) {
|
||
|
|
try {
|
||
|
|
console.log("Submetendo formulário para:", urlWithoutParams);
|
||
|
|
formRef.current.submit();
|
||
|
|
} catch (e) {
|
||
|
|
console.error("Erro ao submeter formulário:", e);
|
||
|
|
errorDetected = true;
|
||
|
|
setError("Erro ao enviar requisição ao servidor.");
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, 50);
|
||
|
|
} else {
|
||
|
|
// Iframe ainda não está pronto, tentar novamente
|
||
|
|
setTimeout(waitForIframeReady, 10);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
// Erro ao acessar contentWindow (pode ser CORS), mas continuar mesmo assim
|
||
|
|
document.body.appendChild(form);
|
||
|
|
setTimeout(() => {
|
||
|
|
if (isMounted && formRef.current) {
|
||
|
|
try {
|
||
|
|
console.log("Submetendo formulário para:", urlWithoutParams);
|
||
|
|
formRef.current.submit();
|
||
|
|
} catch (err) {
|
||
|
|
console.error("Erro ao submeter formulário:", err);
|
||
|
|
errorDetected = true;
|
||
|
|
setError("Erro ao enviar requisição ao servidor.");
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, 100);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Aguardar um pouco antes de verificar se o iframe está pronto
|
||
|
|
setTimeout(waitForIframeReady, 50);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
isMounted = false;
|
||
|
|
clearTimeout(loadTimeout);
|
||
|
|
if (iframeRef.current) {
|
||
|
|
iframeRef.current.removeEventListener("load", handleIframeLoad);
|
||
|
|
iframeRef.current.removeEventListener("error", handleIframeError);
|
||
|
|
}
|
||
|
|
// Remover formulário do body se ainda estiver lá
|
||
|
|
if (formRef.current && formRef.current.parentNode) {
|
||
|
|
formRef.current.parentNode.removeChild(formRef.current);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}, [baseUrl, urlWithoutParams, orderId, model]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="relative w-full h-full" style={{ width, height }}>
|
||
|
|
{/* <iframe
|
||
|
|
// src="http://10.1.1.205:8068/Viewer/InitViewerOrder?orderId=519003838&model=A"
|
||
|
|
src="http://10.1.1.205:8068/Viewer/ViewerEvent?orderId=519003838&model=A"
|
||
|
|
width="100%"
|
||
|
|
height="800"
|
||
|
|
style={{ border: "none" }}
|
||
|
|
/> */}
|
||
|
|
{onClose && (
|
||
|
|
<button
|
||
|
|
onClick={onClose}
|
||
|
|
className="absolute top-2 right-2 z-50 p-2 bg-white rounded-lg shadow-md hover:bg-slate-100 transition-colors"
|
||
|
|
title="Fechar"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-5 h-5 text-slate-600"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth={2}
|
||
|
|
d="M6 18L18 6M6 6l12 12"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{loading && (
|
||
|
|
<div className="absolute inset-0 flex items-center justify-center bg-white/80 z-40">
|
||
|
|
<div className="flex flex-col items-center gap-3">
|
||
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#002147]"></div>
|
||
|
|
<p className="text-sm text-slate-600 font-medium">
|
||
|
|
Carregando relatório...
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{error && (
|
||
|
|
<div className="absolute inset-0 flex items-center justify-center bg-white z-40">
|
||
|
|
<div className="text-center p-6">
|
||
|
|
<p className="text-red-600 font-medium mb-2">{error}</p>
|
||
|
|
<p className="text-sm text-slate-500">URL: {baseUrl}</p>
|
||
|
|
<button
|
||
|
|
onClick={() => {
|
||
|
|
setError(null);
|
||
|
|
setLoading(true);
|
||
|
|
// Recarregar o viewer
|
||
|
|
window.location.reload();
|
||
|
|
}}
|
||
|
|
className="mt-4 px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
|
||
|
|
>
|
||
|
|
Tentar novamente
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div
|
||
|
|
id={viewerId}
|
||
|
|
ref={viewerRef}
|
||
|
|
style={{
|
||
|
|
width: "100%",
|
||
|
|
height: "100%",
|
||
|
|
border: "none",
|
||
|
|
borderRadius: "0 0 1.5rem 1.5rem",
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default StimulsoftViewer;
|