fix: correcao na geracao de relatorio PDF em prioridades de fabricacao

This commit is contained in:
2026-08-21 16:26:20 +00:00
parent 4504c1ded2
commit 046639321b
4 changed files with 190 additions and 322 deletions
+96 -57
View File
@@ -1,17 +1,19 @@
import React from 'react';
import React, { useState } from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { PrioridadesPDFTemplate } from './PrioridadesPDFTemplate';
import { ItemPrioridade } from '@/hooks/useItensPrioridadeFabricacao';
import { Download } from 'lucide-react';
import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
import { Download, Loader2, Printer } from 'lucide-react';
import { generateProfessionalPDF, printProfessionalPDF } from '@/utils/pdfGenerator';
import { toast } from 'sonner';
interface PrioridadesPDFProps {
isOpen: boolean;
onClose: () => void;
itensPorPrioridade: { [key: string]: ItemPrioridade[] };
ofSelecionada?: string | null;
faseSelecionada?: string | null;
versaoAtual?: {
revisao: number;
dataModificacao: string;
@@ -23,64 +25,66 @@ export const PrioridadesPDF: React.FC<PrioridadesPDFProps> = ({
isOpen,
onClose,
itensPorPrioridade,
ofSelecionada,
faseSelecionada,
versaoAtual
}) => {
const [isGenerating, setIsGenerating] = useState(false);
const [isPrinting, setIsPrinting] = useState(false);
const getNomeArquivo = () => {
let nomeArquivo = 'checklist-producao';
const todosItens = Object.values(itensPorPrioridade).flat();
const primeiroItem = todosItens[0];
const of = ofSelecionada || primeiroItem?.peca?.of_number || primeiroItem?.prioridade_fabricacao?.of_number;
const fase = faseSelecionada || primeiroItem?.peca?.etapa_fase || primeiroItem?.prioridade_fabricacao?.etapa_fase;
if (of && fase) {
nomeArquivo = `checklist-producao-${of}-${fase}`;
if (versaoAtual) {
nomeArquivo += `-rev${versaoAtual.revisao}`;
}
}
return nomeArquivo;
};
const handleGerarPDF = async () => {
const elemento = document.getElementById('prioridades-pdf-content');
if (!elemento) {
console.error('Elemento não encontrado');
toast.error('Elemento do relatório não encontrado');
return;
}
try {
const canvas = await html2canvas(elemento, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff'
});
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF('p', 'mm', 'a4');
const imgWidth = 210;
const pageHeight = 295;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
let heightLeft = imgHeight;
let position = 0;
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
while (heightLeft >= 0) {
position = heightLeft - imgHeight;
pdf.addPage();
pdf.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
heightLeft -= pageHeight;
}
// Extrair OF e Fase para o nome do arquivo
const todosItens = Object.values(itensPorPrioridade).flat();
let nomeArquivo = 'checklist-producao';
if (todosItens.length > 0) {
const primeiroItem = todosItens[0];
const of = primeiroItem?.peca?.of_number || primeiroItem?.prioridade_fabricacao?.of_number;
const fase = primeiroItem?.peca?.etapa_fase || primeiroItem?.prioridade_fabricacao?.etapa_fase;
if (of && fase) {
nomeArquivo = `checklist-producao-${of}-${fase}`;
if (versaoAtual) {
nomeArquivo += `-rev${versaoAtual.revisao}`;
}
}
}
pdf.save(`${nomeArquivo}.pdf`);
} catch (error) {
setIsGenerating(true);
const nomeArquivo = getNomeArquivo();
await generateProfessionalPDF('prioridades-pdf-content', `${nomeArquivo}.pdf`);
toast.success('PDF baixado com sucesso!');
} catch (error: any) {
console.error('Erro ao gerar PDF:', error);
toast.error(error.message || 'Erro ao gerar PDF');
} finally {
setIsGenerating(false);
}
};
const handleImprimir = async () => {
const elemento = document.getElementById('prioridades-pdf-content');
if (!elemento) {
toast.error('Elemento do relatório não encontrado');
return;
}
try {
setIsPrinting(true);
await printProfessionalPDF('prioridades-pdf-content');
toast.success('Relatório enviado para impressão');
} catch (error: any) {
console.error('Erro ao imprimir:', error);
toast.error('Erro ao imprimir relatório');
} finally {
setIsPrinting(false);
}
};
@@ -88,18 +92,53 @@ export const PrioridadesPDF: React.FC<PrioridadesPDFProps> = ({
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center justify-between">
Visualizar Checklist de Produção
<Button onClick={handleGerarPDF} className="ml-4">
<Download className="h-4 w-4 mr-2" />
Baixar PDF
</Button>
<DialogTitle className="flex flex-wrap items-center justify-between gap-2">
<span>Visualizar Checklist de Produção</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleImprimir}
disabled={isPrinting || isGenerating}
>
{isPrinting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Imprimindo...
</>
) : (
<>
<Printer className="h-4 w-4 mr-2" />
Imprimir
</>
)}
</Button>
<Button
size="sm"
onClick={handleGerarPDF}
disabled={isGenerating || isPrinting}
>
{isGenerating ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Gerando PDF...
</>
) : (
<>
<Download className="h-4 w-4 mr-2" />
Baixar PDF
</>
)}
</Button>
</div>
</DialogTitle>
</DialogHeader>
<div className="mt-4">
<PrioridadesPDFTemplate
itensPorPrioridade={itensPorPrioridade}
ofSelecionada={ofSelecionada}
faseSelecionada={faseSelecionada}
versaoAtual={versaoAtual}
/>
</div>
@@ -4,6 +4,8 @@ import { ItemPrioridade } from '@/hooks/useItensPrioridadeFabricacao';
interface PrioridadesPDFTemplateProps {
itensPorPrioridade: { [key: string]: ItemPrioridade[] };
ofSelecionada?: string | null;
faseSelecionada?: string | null;
versaoAtual?: {
revisao: number;
dataModificacao: string;
@@ -13,13 +15,15 @@ interface PrioridadesPDFTemplateProps {
export const PrioridadesPDFTemplate: React.FC<PrioridadesPDFTemplateProps> = ({
itensPorPrioridade,
ofSelecionada,
faseSelecionada,
versaoAtual
}) => {
const todosItens = Object.values(itensPorPrioridade).flat();
const primeiroItem = todosItens[0];
const ofNumber = primeiroItem?.peca?.of_number || primeiroItem?.prioridade_fabricacao?.of_number || 'N/A';
const etapaFase = primeiroItem?.peca?.etapa_fase || primeiroItem?.prioridade_fabricacao?.etapa_fase || 'N/A';
const ofNumber = ofSelecionada || primeiroItem?.peca?.of_number || primeiroItem?.prioridade_fabricacao?.of_number || 'N/A';
const etapaFase = faseSelecionada || primeiroItem?.peca?.etapa_fase || primeiroItem?.prioridade_fabricacao?.etapa_fase || 'N/A';
const dataAtual = new Date().toLocaleDateString('pt-BR');
+2 -160
View File
@@ -420,168 +420,10 @@ const PrioridadesFabricacao = () => {
isOpen={showPrioridadesPDF}
onClose={() => setShowPrioridadesPDF(false)}
itensPorPrioridade={itensPorPrioridade}
ofSelecionada={ofSelecionada}
faseSelecionada={faseSelecionada}
versaoAtual={versaoAtual}
/>
{/* Componente oculto para impressão */}
<div className="hidden">
<div id="prioridades-pdf-content">
{(ofSelecionada && faseSelecionada) && (
<div className="bg-white text-black max-w-4xl mx-auto p-6">
{/* Usar o mesmo template do PDF */}
<div className="flex justify-between items-center border-b-2 border-gray-800 pb-4 mb-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">Checklist de Produção</h1>
<p className="text-gray-600">Formulário para apontamento da fabricação.</p>
</div>
<div className="text-right">
<p className="font-semibold">
Data de Emissão: <span className="font-normal">{new Date().toLocaleDateString('pt-BR')}</span>
{versaoAtual && (
<span className="ml-2 text-gray-500">Rev. {versaoAtual.revisao}</span>
)}
</p>
</div>
</div>
<div className="border border-gray-200 bg-white p-4 rounded-lg mb-2">
<div className="grid grid-cols-1 md:grid-cols-4 gap-x-6 gap-y-4">
<div>
<p className="text-xs font-medium text-gray-500">Ordem de Fabricação (OF)</p>
<p className="text-base font-bold text-gray-800">{ofSelecionada}</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500">Fase</p>
<p className="text-base font-bold text-gray-800">{faseSelecionada}</p>
</div>
<div className="md:col-span-2">
<p className="text-xs font-medium text-gray-500">PROCESSO</p>
<div className="flex items-center flex-wrap gap-x-4 gap-y-1 mt-1">
{['Corte', 'Solda', 'Pintura', 'Expedição'].map((processo) => (
<div key={processo} className="flex items-center gap-1">
<div className="w-4 h-4 border-2 border-gray-500"></div>
<span className="text-sm font-semibold text-gray-700">{processo}</span>
</div>
))}
</div>
</div>
</div>
</div>
<div className="text-xs text-gray-600 mb-6 flex items-center flex-wrap gap-x-3">
<span className="font-semibold">Legenda:</span>
<span>Marca (Qtd)</span>
<span className="font-medium text-gray-500">(S/M)</span>
<span>= Sem Montagem,</span>
<span className="font-medium text-gray-500">(C/M)</span>
<span>= Com Montagem. Os quadrados indicam o controle de peças fabricadas.</span>
</div>
{/* Renderizar os itens por prioridade */}
<div className="space-y-8">
{['P1', 'P2', 'P3', 'P4'].map((codigo, priorityIndex) => {
const itens = itensPorPrioridade[codigo] || [];
if (itens.length === 0) return null;
const getPrioridadeNome = (codigo: string) => {
switch (codigo) {
case 'P1': return 'Prioridade P1 - Urgente';
case 'P2': return 'Prioridade P2 - Alta';
case 'P3': return 'Prioridade P3 - Média';
case 'P4': return 'Prioridade P4 - Baixa';
default: return 'Desconhecida';
}
};
const getCoresPrioridade = (codigo: string) => {
switch (codigo) {
case 'P1': return 'text-red-700 bg-red-100';
case 'P2': return 'text-orange-700 bg-orange-100';
case 'P3': return 'text-blue-700 bg-blue-100';
case 'P4': return 'text-gray-700 bg-gray-200';
default: return 'text-gray-700 bg-gray-200';
}
};
return (
<div key={codigo}>
<h2 className={`text-lg font-semibold ${getCoresPrioridade(codigo)} px-3 py-1 rounded-md inline-block mb-3`}>
{getPrioridadeNome(codigo)}
</h2>
<div className="space-y-1">
{Array.from({ length: Math.ceil(itens.length / 3) }, (_, i) => {
const bgColorClass = i % 2 !== 0 ? 'bg-gray-50' : 'bg-white';
const rowItems = itens.slice(i * 3, (i + 1) * 3);
return (
<div key={i} className={`grid grid-cols-3 gap-2 p-1 rounded-md ${bgColorClass}`}>
{rowItems.map((item) => {
const quantidade = item.quantidade_priorizada;
const marca = item.peca?.marca || 'N/A';
const temComponentes = item.peca?.tem_componentes;
const infoType = temComponentes ? '(C/M)' : '(S/M)';
// Gerar checkboxes
const generateTickBoxes = (quantity: number) => {
const boxes = [];
if (quantity > 10) {
const numBigBoxes = Math.floor(quantity / 5);
const numSmallBoxes = quantity % 5;
for (let i = 0; i < numBigBoxes; i++) {
boxes.push(
<div key={`big-${i}`} className="tick-box-large">
<span>5</span>
</div>
);
}
for (let i = 0; i < numSmallBoxes; i++) {
boxes.push(<div key={`small-${i}`} className="tick-box"></div>);
}
} else {
for (let i = 0; i < quantity; i++) {
boxes.push(<div key={i} className="tick-box"></div>);
}
}
return <div className="flex items-center flex-wrap gap-1">{boxes}</div>;
};
return (
<div key={item.id} className="item-card">
<div className="flex items-center flex-wrap gap-2 mb-2">
<span className="font-semibold text-sm whitespace-nowrap">
{marca} ({quantidade})
</span>
<span className="text-xs font-medium text-gray-500">{infoType}</span>
{generateTickBoxes(quantidade)}
</div>
<div className="mt-2 text-xs">
<div className="border-b border-gray-400 pb-1 h-5">Data/Operador:</div>
</div>
</div>
);
})}
{/* Preencher células vazias se necessário */}
{Array.from({ length: 3 - rowItems.length }, (_, emptyIndex) => (
<div key={`empty-${emptyIndex}`}></div>
))}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
</StandardPageLayout>
);
+86 -103
View File
@@ -2,75 +2,79 @@ import html2canvas from 'html2canvas';
import jsPDF from 'jspdf';
export const generateProfessionalPDF = async (elementId: string, filename: string) => {
let tempContainer: HTMLElement | null = null;
try {
const element = document.getElementById(elementId);
if (!element) {
throw new Error('Elemento não encontrado para gerar PDF');
throw new Error(`Elemento #${elementId} não encontrado para gerar PDF`);
}
// Aguardar um pouco para garantir que o elemento esteja completamente renderizado
await new Promise(resolve => setTimeout(resolve, 200));
// Criar um container temporário isolado fora de modais/dialogs para evitar bugs de tamanho 0x0
tempContainer = document.createElement('div');
tempContainer.style.position = 'fixed';
tempContainer.style.left = '0';
tempContainer.style.top = '0';
tempContainer.style.width = '850px';
tempContainer.style.backgroundColor = '#ffffff';
tempContainer.style.zIndex = '-9999';
tempContainer.style.opacity = '1';
tempContainer.style.pointerEvents = 'none';
tempContainer.style.margin = '0';
tempContainer.style.padding = '20px';
tempContainer.style.boxSizing = 'border-box';
tempContainer.style.overflow = 'visible';
// Garantir que o elemento esteja visível e com dimensões corretas
const originalDisplay = element.style.display;
const originalVisibility = element.style.visibility;
const originalPosition = element.style.position;
element.style.display = 'block';
element.style.visibility = 'visible';
element.style.position = 'relative';
// Forçar um reflow
element.offsetHeight;
// Aguardar mais um pouco após forçar o reflow
// Clonar o elemento para o container isolado
const clonedElement = element.cloneNode(true) as HTMLElement;
clonedElement.style.display = 'block';
clonedElement.style.visibility = 'visible';
clonedElement.style.position = 'static';
clonedElement.style.width = '100%';
clonedElement.style.maxWidth = '100%';
clonedElement.style.height = 'auto';
clonedElement.style.maxHeight = 'none';
clonedElement.style.overflow = 'visible';
clonedElement.style.transform = 'none';
clonedElement.style.backgroundColor = '#ffffff';
clonedElement.style.color = '#000000';
tempContainer.appendChild(clonedElement);
document.body.appendChild(tempContainer);
// Aguardar renderização e computação de layout
await new Promise(resolve => setTimeout(resolve, 300));
console.log('Gerando PDF para elemento:', elementId);
console.log('Dimensões do elemento:', {
width: element.offsetWidth,
height: element.offsetHeight,
scrollWidth: element.scrollWidth,
scrollHeight: element.scrollHeight
});
// Se o elemento não tem dimensões, isso pode causar PDF em branco
if (element.offsetWidth === 0 || element.offsetHeight === 0) {
throw new Error('Elemento tem dimensões zero - não é possível gerar PDF');
}
// Elemento a ser renderizado pelo html2canvas
const targetElement = (clonedElement.offsetHeight > 0 && clonedElement.offsetWidth > 0)
? clonedElement
: element;
// Configurações otimizadas para html2canvas
const canvas = await html2canvas(element, {
const canvas = await html2canvas(targetElement, {
scale: 2,
useCORS: true,
allowTaint: false,
allowTaint: true,
backgroundColor: '#ffffff',
width: element.scrollWidth,
height: element.scrollHeight,
scrollX: 0,
scrollY: 0,
windowWidth: Math.max(element.scrollWidth, 1200),
windowHeight: Math.max(element.scrollHeight, 800),
foreignObjectRendering: false,
removeContainer: false,
imageTimeout: 10000,
logging: false
logging: false,
windowWidth: 1024,
onclone: (clonedDoc) => {
const found = clonedDoc.getElementById(elementId) || clonedDoc.body;
if (found) {
(found as HTMLElement).style.display = 'block';
(found as HTMLElement).style.visibility = 'visible';
}
}
});
console.log('Canvas criado com sucesso:', {
width: canvas.width,
height: canvas.height
});
// Verificar se o canvas foi criado corretamente
if (canvas.width === 0 || canvas.height === 0) {
throw new Error('Canvas criado com dimensões zero');
if (!canvas || canvas.width === 0 || canvas.height === 0) {
throw new Error('Não foi possível capturar o layout visual para o PDF');
}
// Restaurar estilos originais
element.style.display = originalDisplay;
element.style.visibility = originalVisibility;
element.style.position = originalPosition;
const imgData = canvas.toDataURL('image/png', 1.0);
if (!imgData || !imgData.startsWith('data:image/png;base64,')) {
throw new Error('Falha ao processar os dados da imagem para o PDF');
}
// Criar PDF com configurações otimizadas
const pdf = new jsPDF({
@@ -85,10 +89,10 @@ export const generateProfessionalPDF = async (elementId: string, filename: strin
const pageHeight = 297;
// Margens adequadas
const marginTop = 15;
const marginBottom = 15;
const marginLeft = 15;
const marginRight = 15;
const marginTop = 12;
const marginBottom = 12;
const marginLeft = 12;
const marginRight = 12;
// Área útil para conteúdo
const contentWidth = pageWidth - marginLeft - marginRight;
@@ -98,103 +102,82 @@ export const generateProfessionalPDF = async (elementId: string, filename: strin
const imgWidth = contentWidth;
const imgHeight = (canvas.height * contentWidth) / canvas.width;
// Converter canvas para imagem
const imgData = canvas.toDataURL('image/png', 1.0);
console.log('Adicionando imagem ao PDF:', {
imgWidth,
imgHeight,
contentHeight,
totalPages: Math.ceil(imgHeight / contentHeight)
});
// Verificar se os dados da imagem foram gerados
if (!imgData || imgData === 'data:,') {
throw new Error('Falha ao gerar dados da imagem do canvas');
}
// Sistema de paginação
let currentY = 0;
let pageNumber = 1;
const totalPages = Math.ceil(imgHeight / contentHeight);
const totalPages = Math.max(1, Math.ceil(imgHeight / contentHeight));
// Função para adicionar rodapé com numeração
const addFooter = (pageNum: number, totalPages: number) => {
const addFooter = (pageNum: number, totalPagesCount: number) => {
pdf.setFontSize(8);
pdf.setTextColor(100, 100, 100);
const footerText = `Página ${pageNum} de ${totalPages}`;
pdf.setTextColor(120, 120, 120);
const footerText = `Página ${pageNum} de ${totalPagesCount}`;
const textWidth = pdf.getTextWidth(footerText);
const footerX = (pageWidth - textWidth) / 2;
const footerY = pageHeight - 8;
const footerY = pageHeight - 6;
pdf.text(footerText, footerX, footerY);
};
// Primeira página
if (imgHeight <= contentHeight) {
// Conteúdo cabe em uma página
pdf.addImage(imgData, 'PNG', marginLeft, marginTop, imgWidth, imgHeight);
pdf.addImage(imgData, 'PNG', marginLeft, marginTop, imgWidth, imgHeight, undefined, 'FAST');
addFooter(1, 1);
} else {
// Conteúdo precisa de múltiplas páginas
// Conteúdo precisa de múltiplas páginas com corte preciso
while (currentY < imgHeight) {
if (pageNumber > 1) {
pdf.addPage();
}
// Calcular a altura restante do conteúdo
const remainingHeight = imgHeight - currentY;
const currentPageHeight = Math.min(contentHeight, remainingHeight);
// Criar um canvas temporário para a seção atual
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
if (tempCtx) {
if (tempCtx && currentPageHeight > 0) {
const sliceHeight = Math.round((currentPageHeight * canvas.width) / imgWidth);
tempCanvas.width = canvas.width;
tempCanvas.height = (currentPageHeight * canvas.width) / imgWidth;
tempCanvas.height = sliceHeight;
tempCtx.fillStyle = '#ffffff';
tempCtx.fillRect(0, 0, tempCanvas.width, tempCanvas.height);
// Desenhar a seção atual do canvas original
tempCtx.drawImage(
canvas,
0,
(currentY * canvas.width) / imgWidth,
Math.round((currentY * canvas.width) / imgWidth),
canvas.width,
tempCanvas.height,
sliceHeight,
0,
0,
canvas.width,
tempCanvas.height
sliceHeight
);
// Converter para dados de imagem
const tempImgData = tempCanvas.toDataURL('image/png', 1.0);
// Adicionar a seção ao PDF
pdf.addImage(tempImgData, 'PNG', marginLeft, marginTop, imgWidth, currentPageHeight);
if (tempImgData && tempImgData.startsWith('data:image/png;base64,')) {
pdf.addImage(tempImgData, 'PNG', marginLeft, marginTop, imgWidth, currentPageHeight, undefined, 'FAST');
}
}
// Adicionar rodapé
addFooter(pageNumber, totalPages);
// Preparar para próxima página
currentY += contentHeight;
pageNumber++;
}
}
// Forçar o download do PDF
console.log('Iniciando download do PDF:', filename);
// Salvar arquivo PDF
pdf.save(filename);
// Aguardar um momento para garantir que o download seja iniciado
await new Promise(resolve => setTimeout(resolve, 500));
console.log('PDF salvo com sucesso:', filename);
return true;
} catch (error) {
} catch (error: any) {
console.error('Erro detalhado ao gerar PDF:', error);
throw new Error(`Erro ao gerar PDF: ${error.message}`);
throw new Error(`Erro ao gerar PDF: ${error.message || error}`);
} finally {
if (tempContainer && tempContainer.parentNode) {
tempContainer.parentNode.removeChild(tempContainer);
}
}
};