first commit

This commit is contained in:
rodrigo committed 2025-04-10 23:07:21 -03:00
commit c9fb67e751
423 files changed
+53287

No files matched your search

@@ -0,0 +1,344 @@
import { useState, useEffect } from 'react';
import { api } from '../../lib/api';
import { toast } from 'react-toastify';
import { Dialog } from '@headlessui/react';
import { DocumentArrowUpIcon } from '@heroicons/react/24/outline';
import { Editor } from '@tinymce/tinymce-react';
import { DOCUMENT_VARIABLES } from '../../utils/documentVariables';
// Definir interface para o estado
interface ConfigState {
timbradoUrl: string;
timbradoMarginTop: number;
timbradoMarginBottom: number;
timbradoMarginLeft: number;
timbradoMarginRight: number;
templatePadrao: string;
margensTemplate: {
top: number;
bottom: number;
left: number;
right: number;
};
}
export function ConfiguracaoDocumento() {
const [config, setConfig] = useState<ConfigState>({
timbradoUrl: '',
timbradoMarginTop: 100,
timbradoMarginBottom: 50,
timbradoMarginLeft: 50,
timbradoMarginRight: 50,
templatePadrao: '',
margensTemplate: {
top: 35,
bottom: 29,
left: 25,
right: 25
}
});
const [isPreviewOpen, setPreviewOpen] = useState(false);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
loadConfig();
}, []);
const loadConfig = async () => {
try {
const response = await api.get('/documentos/configuracoes');
console.log('Configurações carregadas:', response.data); // Debug
if (response.data) {
setConfig({
timbradoUrl: response.data.timbradoUrl || '',
timbradoMarginTop: response.data.timbradoMarginTop || 100,
timbradoMarginBottom: response.data.timbradoMarginBottom || 50,
timbradoMarginLeft: response.data.timbradoMarginLeft || 50,
timbradoMarginRight: response.data.timbradoMarginRight || 50,
templatePadrao: response.data.templatePadrao || '',
margensTemplate: {
top: response.data.margensTemplate?.top || 35,
bottom: response.data.margensTemplate?.bottom || 29,
left: response.data.margensTemplate?.left || 25,
right: response.data.margensTemplate?.right || 25
}
});
}
} catch (error) {
console.error('Erro ao carregar configurações:', error);
toast.error('Erro ao carregar configurações');
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Atualizado para aceitar PDF
if (!['image/png', 'image/svg+xml', 'application/pdf'].includes(file.type)) {
toast.error('Por favor, use apenas arquivos PDF, PNG ou SVG');
return;
}
if (file.size > 5 * 1024 * 1024) { // Aumentado para 5MB para acomodar PDFs
toast.error('O arquivo deve ter no máximo 5MB');
return;
}
try {
const formData = new FormData();
formData.append('timbrado', file);
const response = await api.post('/documentos/upload-timbrado', formData);
setConfig(prev => ({ ...prev, timbradoUrl: response.data.url }));
toast.success('Timbrado atualizado com sucesso');
} catch (error) {
toast.error('Erro ao fazer upload do timbrado');
}
};
const handleSave = async () => {
try {
setIsSaving(true);
await api.post('/documentos/configuracoes', config);
toast.success('Configurações salvas com sucesso');
} catch (error) {
toast.error('Erro ao salvar configurações');
} finally {
setIsSaving(false);
}
};
const handleEditorSetup = (editor: any) => {
editor.ui.registry.addMenuButton('variables', {
text: 'Variáveis',
fetch: (callback: Function) => {
const items = Object.entries(DOCUMENT_VARIABLES).flatMap(([category, vars]) =>
Object.entries(vars).map(([name, value]) => ({
type: 'menuitem',
text: `${category} - ${name.replace(/_/g, ' ')}`,
onAction: () => editor.insertContent(value as string)
}))
);
callback(items);
}
});
};
// Modificar a configuração do TinyMCE para remover o plugin 'template'
const editorConfig = {
height: 400,
menubar: true,
plugins: [
'code', 'preview' // Removido 'template'
],
toolbar: 'variables | undo redo | formatselect | ' +
'bold italic | alignleft aligncenter alignright | ' +
'code preview',
setup: handleEditorSetup
};
return (
<div className="p-6 bg-white dark:bg-gray-800 rounded-lg shadow">
<h2 className="text-xl font-bold mb-6">Configuração do Papel Timbrado</h2>
<div className="space-y-6">
{/* Upload do Timbrado */}
<div>
<label className="block mb-2">Papel Timbrado</label>
<div className="flex items-center gap-4">
<label className="flex items-center px-4 py-2 bg-blue-600 text-white rounded cursor-pointer hover:bg-blue-700">
<DocumentArrowUpIcon className="w-5 h-5 mr-2" />
Fazer Upload
<input
type="file"
className="hidden"
accept=".pdf,.png,.svg"
onChange={handleFileUpload}
/>
</label>
{config.timbradoUrl && (
<button
onClick={() => setPreviewOpen(true)}
className="text-blue-600 hover:underline"
>
Visualizar
</button>
)}
</div>
<p className="mt-2 text-sm text-gray-500">
Use arquivos PDF, PNG ou SVG. Tamanho máximo: 5MB
</p>
</div>
{/* Margens */}
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block mb-2">Margem Superior (px)</label>
<input
type="number"
value={config.timbradoMarginTop}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginTop: Number(e.target.value) }))}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-2">Margem Inferior (px)</label>
<input
type="number"
value={config.timbradoMarginBottom}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginBottom: Number(e.target.value) }))}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-2">Margem Esquerda (px)</label>
<input
type="number"
value={config.timbradoMarginLeft}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginLeft: Number(e.target.value) }))}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-2">Margem Direita (px)</label>
<input
type="number"
value={config.timbradoMarginRight}
onChange={e => setConfig(prev => ({ ...prev, timbradoMarginRight: Number(e.target.value) }))}
className="w-full p-2 border rounded"
/>
</div>
</div>
{/* Template Padrão */}
<div className="mt-6">
<label className="block mb-2">Template Padrão</label>
<Editor
apiKey={import.meta.env.VITE_TINYMCE_API_KEY}
value={config.templatePadrao}
onEditorChange={(content) => setConfig(prev => ({ ...prev, templatePadrao: content }))}
init={editorConfig}
/>
<div className="mt-2 p-4 bg-gray-50 rounded border">
<h4 className="font-medium mb-2">Variáveis Disponíveis:</h4>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{Object.entries(DOCUMENT_VARIABLES).map(([category, vars]) => (
<div key={category}>
<h5 className="font-medium text-sm text-gray-700">{category}</h5>
<ul className="mt-1 space-y-1">
{Object.entries(vars).map(([name, value]) => (
<li key={name} className="text-xs text-gray-600">
{value as string}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
{/* Margens do Template */}
<div className="mt-6 grid grid-cols-2 gap-4">
<div>
<label className="block mb-2">Margem Superior (mm)</label>
<input
type="number"
value={config.margensTemplate.top}
onChange={e => setConfig(prev => ({
...prev,
margensTemplate: {
...prev.margensTemplate,
top: Number(e.target.value)
}
}))}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-2">Margem Inferior (mm)</label>
<input
type="number"
value={config.margensTemplate.bottom}
onChange={e => setConfig(prev => ({
...prev,
margensTemplate: {
...prev.margensTemplate,
bottom: Number(e.target.value)
}
}))}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-2">Margem Esquerda (mm)</label>
<input
type="number"
value={config.margensTemplate.left}
onChange={e => setConfig(prev => ({
...prev,
margensTemplate: {
...prev.margensTemplate,
left: Number(e.target.value)
}
}))}
className="w-full p-2 border rounded"
/>
</div>
<div>
<label className="block mb-2">Margem Direita (mm)</label>
<input
type="number"
value={config.margensTemplate.right}
onChange={e => setConfig(prev => ({
...prev,
margensTemplate: {
...prev.margensTemplate,
right: Number(e.target.value)
}
}))}
className="w-full p-2 border rounded"
/>
</div>
</div>
<button
onClick={handleSave}
disabled={isSaving}
className={`px-4 py-2 ${
isSaving
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
} text-white rounded`}
>
{isSaving ? 'Salvando...' : 'Salvar Configurações'}
</button>
</div>
{/* Modal de Preview */}
<Dialog open={isPreviewOpen} onClose={() => setPreviewOpen(false)}>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-3xl bg-white rounded-lg shadow-xl p-6">
<h3 className="text-lg font-semibold mb-4">Preview do Timbrado</h3>
<div className="border rounded p-4 bg-gray-50">
<img
src={config.timbradoUrl}
alt="Timbrado"
className="max-w-full h-auto"
/>
</div>
<button
onClick={() => setPreviewOpen(false)}
className="mt-4 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Fechar
</button>
</Dialog.Panel>
</div>
</Dialog>
</div>
);
}
@@ -0,0 +1,301 @@
import { useState, useEffect } from 'react';
import { Editor } from '@tinymce/tinymce-react';
import { Button, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
import { Save as SaveIcon } from '@mui/icons-material';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { toast } from 'react-toastify'; // Adicionar esta importação
import { api } from '../../lib/api'; // Adicionar esta importação
import { DocumentoService } from '../../services/DocumentoService'; // Adicionar esta importação
interface DocumentoEditorProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: any) => Promise<void>;
initialData?: any;
processoId?: string;
atendimentoId?: string;
documento?: any; // Adicionar esta prop
}
interface TemplateVariables {
cliente: any;
advogado: any;
escritorio: any;
[key: string]: any;
}
const preencherTemplate = (template: string, variables: TemplateVariables) => {
let result = template;
Object.entries(variables).forEach(([key, value]) => {
const regex = new RegExp(`{{${key}}}`, 'g');
result = result.replace(regex, value);
});
return result;
};
export function DocumentoEditor({
isOpen,
onClose,
onSave,
initialData,
processoId,
atendimentoId,
documento // Recuperar a prop
}: DocumentoEditorProps) {
const [content, setContent] = useState('');
const [nome, setNome] = useState('');
const [tipo, setTipo] = useState('OUTROS');
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [selectedAtendimento, setSelectedAtendimento] = useState(null);
const [atendimentos, setAtendimentos] = useState([]);
const [templates, setTemplates] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
if (initialData) {
setContent(initialData.conteudo || '');
setNome(initialData.nome || '');
setTipo(initialData.tipo || 'OUTROS');
}
}, [initialData]);
useEffect(() => {
const loadData = async () => {
try {
const [atendimentosRes, templatesRes] = await Promise.all([
api.get('/atendimentos'),
api.get('/documentos-modelos')
]);
setAtendimentos(atendimentosRes.data);
setTemplates(templatesRes.data);
} catch (error) {
toast.error('Erro ao carregar dados');
} finally {
setIsLoading(false);
}
};
loadData();
}, []);
useEffect(() => {
if (documento) {
setContent(documento.conteudo || '');
setNome(documento.nome || '');
setTipo(documento.tipo || 'OUTROS');
}
}, [documento]);
const handleTemplateSelect = async (templateId) => {
try {
const template = templates.find(t => t.id === templateId);
if (!template) return;
const atendimento = atendimentos.find(a => a.id === selectedAtendimento);
if (!atendimento) return;
// Buscar dados do atendimento com as relações necessárias
const [atendimentoDetalhado, usuarioRes, configRes] = await Promise.all([
api.get(`/atendimentos/${atendimento.id}`),
api.get('/auth/me'),
api.get('/documentos/configuracoes')
]);
// Formatar endereço completo e outros dados do cliente
const cliente = atendimentoDetalhado.data.cliente;
const enderecoCompleto = `${cliente.endereco}, ${cliente.numero}${cliente.complemento ? `, ${cliente.complemento}` : ''}, ${cliente.bairro}, ${cliente.cidade}/${cliente.estado}, CEP: ${cliente.cep}`;
// Formatar data extenso corretamente
const formatadorData = new Intl.DateTimeFormat('pt-BR', {
day: 'numeric',
month: 'long',
year: 'numeric'
});
// Preparar variáveis com dados formatados
const variables = {
cliente: {
...cliente,
enderecoCompleto,
nome: cliente.nome || '',
cpf: cliente.cpfCnpj || '', // Adicionar cpf como alias para cpfCnpj
cpfCnpj: cliente.cpfCnpj || '', // Manter cpfCnpj também
rg: cliente.rg || '',
estadoCivil: cliente.estadoCivil || '',
profissao: cliente.profissao || '',
nacionalidade: cliente.nacionalidade || 'brasileiro(a)',
nomePai: cliente.nomePai || '',
nomeMae: cliente.nomeMae || ''
},
atendimento: atendimentoDetalhado.data,
processo: atendimentoDetalhado.data.processo,
advogado: {
...usuarioRes.data,
oabCompleta: `${usuarioRes.data.oab}/${usuarioRes.data.oabUF}`
},
escritorio: configRes.data,
data: {
atual: new Date().toLocaleDateString('pt-BR'),
atualExtenso: formatadorData.format(new Date()), // Formatar a data corretamente
dia: new Date().getDate().toString(),
mes: (new Date().getMonth() + 1).toString(),
mesExtenso: new Intl.DateTimeFormat('pt-BR', { month: 'long' }).format(new Date()),
ano: new Date().getFullYear().toString()
}
};
// Teste para debug
console.log('Data por extenso:', variables.data.atualExtenso);
// Preencher template
const conteudoPreenchido = template.conteudo.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
try {
const valor = path.split('.').reduce((obj, key) => obj?.[key], variables);
return valor !== undefined ? valor : match;
} catch (error) {
console.warn(`Erro ao processar variável ${path}:`, error);
return match;
}
});
setContent(conteudoPreenchido);
setNome(`${template.nome} - ${cliente.nome}`);
setTipo(template.tipo);
// Debug para verificar substituições
console.log('Template original:', template.conteudo);
console.log('Conteúdo processado:', conteudoPreenchido);
} catch (error) {
console.error('Erro ao carregar template:', error);
toast.error('Erro ao carregar template. Verifique se o atendimento possui um cliente vinculado.');
}
};
const handleSave = async () => {
// Verificação de atendimento apenas para novos documentos
if (!documento && !selectedAtendimento) {
toast.error('Selecione um atendimento');
return;
}
try {
setIsSaving(true);
await onSave({
nome,
tipo,
conteudo: content,
atendimentoId: documento?.atendimentoId || selectedAtendimento,
processoId: documento?.processoId || processoId
});
toast.success('Documento salvo com sucesso');
onClose();
} catch (error) {
toast.error('Erro ao salvar documento');
} finally {
setIsSaving(false);
}
};
return (
<Dialog open={isOpen} onClose={onClose} maxWidth="lg" fullWidth>
<DialogTitle className="sticky top-0 bg-white z-10 border-b">
<div className="flex flex-col gap-4">
<div className="flex gap-4">
<select
value={selectedAtendimento || ''}
onChange={(e) => setSelectedAtendimento(e.target.value)}
className="flex-1 p-2 border rounded"
>
<option value="">Selecione um atendimento</option>
{atendimentos.map(atendimento => (
<option key={atendimento.id} value={atendimento.id}>
{atendimento.protocolo} - {atendimento.cliente.nome}
</option>
))}
</select>
<select
value={selectedTemplate || ''}
onChange={(e) => handleTemplateSelect(e.target.value)}
className="flex-1 p-2 border rounded"
>
<option value="">Selecione um template</option>
{templates.map(template => (
<option key={template.id} value={template.id}>
{template.nome}
</option>
))}
</select>
</div>
<div className="flex-1">
<input
type="text"
value={nome}
onChange={(e) => setNome(e.target.value)}
placeholder="Nome do documento"
className="w-full px-2 py-1 border border-gray-300 rounded"
/>
</div>
<select
value={tipo}
onChange={(e) => setTipo(e.target.value)}
className="ml-4 px-2 py-1 border border-gray-300 rounded"
>
<option value="PETICAO">Petição</option>
<option value="CONTRATO">Contrato</option>
<option value="PROCURACAO">Procuração</option>
<option value="DECLARACAO">Declaração</option>
<option value="OUTROS">Outros</option>
</select>
</div>
</DialogTitle>
<DialogContent className="max-h-[80vh] overflow-y-auto">
<Editor
apiKey={import.meta.env.VITE_TINYMCE_API_KEY}
init={{
height: 500,
menubar: true,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount'
],
toolbar: 'undo redo | formatselect | ' +
'bold italic backcolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
language: 'pt_BR', // Adicionar configuração de idioma
language_url: '/tinymce/langs/pt_BR.js', // Adicionar URL do arquivo de idioma
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }',
// Adicionar estas configurações
promotion: false,
branding: false,
preview_styles: false,
referrer_policy: 'origin',
disable_stats: true // Desabilitar estatísticas
}}
value={content}
onEditorChange={setContent}
/>
</DialogContent>
<DialogActions className="sticky bottom-0 bg-white z-10 border-t px-4 py-3">
<Button onClick={onClose} startIcon={<XMarkIcon className="w-4 h-4" />}>
Cancelar
</Button>
<Button
onClick={handleSave}
disabled={isSaving || !nome}
startIcon={<SaveIcon className="w-4 h-4" />}
variant="contained"
>
Salvar
</Button>
</DialogActions>
</Dialog>
);
}
@@ -0,0 +1,230 @@
import { Dialog } from '@headlessui/react';
import { XMarkIcon, PrinterIcon, DocumentArrowDownIcon } from '@heroicons/react/24/outline';
import { api } from '../../lib/api';
import { toast } from 'react-toastify';
import { useState } from 'react';
interface DocumentoViewerProps {
documento: any;
isOpen: boolean;
onClose: () => void;
}
export function DocumentoViewer({ documento, isOpen, onClose }: DocumentoViewerProps) {
const [isExporting, setIsExporting] = useState(false);
const handlePrint = () => {
const printWindow = window.open('', '_blank');
if (!printWindow) return;
const html = `
<!DOCTYPE html>
<html>
<head>
<title>${documento?.nome || 'Documento'}</title>
<style>
@page {
size: A4;
margin: 3cm 2cm 2cm 3cm;
}
body {
font-family: Arial, sans-serif;
line-height: 1.6;
margin: 0;
padding: 0;
background: white;
font-size: 12pt;
}
.header {
position: running(header);
width: 100%;
text-align: center;
padding-bottom: 10px;
border-bottom: 1px solid #ccc;
}
.footer {
position: running(footer);
width: 100%;
text-align: center;
font-size: 10pt;
padding-top: 10px;
border-top: 1px solid #ccc;
}
@page {
@top-center { content: element(header) }
@bottom-center { content: element(footer) }
}
.content {
margin: 0 auto;
max-width: 100%;
}
h1 {
font-size: 16pt;
text-align: center;
margin: 2em 0;
}
p {
text-align: justify;
margin: 1em 0;
}
.assinatura {
margin-top: 4em;
text-align: center;
}
.local-data {
text-align: right;
margin: 3em 0;
}
</style>
</head>
<body>
<div class="header">
<h2 style="margin:0;font-size:14pt">${documento?.escritorio?.nome || ''}</h2>
</div>
<div class="content">
${documento?.conteudo || ''}
</div>
<div class="footer">
${documento?.escritorio?.endereco || ''}<br>
Tel: ${documento?.escritorio?.telefone || ''} | Email: ${documento?.escritorio?.email || ''}
</div>
<script>
window.onload = function() {
window.print();
setTimeout(window.close, 500);
};
</script>
</body>
</html>
`;
printWindow.document.write(html);
printWindow.document.close();
};
const handleExportPDF = async () => {
try {
setIsExporting(true);
toast.info('Gerando PDF, aguarde...', { autoClose: false, toastId: 'generating-pdf' });
// Buscar configurações atualizadas do escritório
const response = await api.post('/documentos/exportar-pdf', {
conteudo: documento.conteudo,
escritorio: documento.escritorio
}, {
responseType: 'blob',
headers: {
'Accept': 'application/pdf'
}
});
// Criar e baixar o arquivo
const blob = new Blob([response.data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${documento.nome || 'documento'}.pdf`);
document.body.appendChild(link);
link.click();
// Limpeza
window.URL.revokeObjectURL(url);
document.body.removeChild(link);
toast.dismiss('generating-pdf');
toast.success('PDF gerado com sucesso!');
} catch (error) {
console.error('Erro ao gerar PDF:', error);
toast.dismiss('generating-pdf');
toast.error('Erro ao gerar o PDF');
} finally {
setIsExporting(false);
}
};
return (
<Dialog
open={isOpen}
onClose={onClose}
className="relative z-50"
>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-4xl bg-white rounded-lg shadow-xl">
<div className="flex items-center justify-between p-4 border-b">
<Dialog.Title className="text-lg font-semibold">
{documento?.nome}
</Dialog.Title>
<div className="flex items-center gap-2">
<button
onClick={handleExportPDF}
disabled={isExporting}
className={`inline-flex items-center px-3 py-2 ${
isExporting
? 'bg-blue-400 cursor-not-allowed'
: 'bg-blue-600 hover:bg-blue-700'
} text-white rounded`}
>
{isExporting ? (
<>
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Gerando PDF...
</>
) : (
<>
<DocumentArrowDownIcon className="w-5 h-5 mr-2" />
Exportar PDF
</>
)}
</button>
<button
onClick={onClose}
className="inline-flex items-center px-3 py-2 bg-gray-100 text-gray-600 rounded hover:bg-gray-200"
>
<XMarkIcon className="w-5 h-5 mr-2" />
Fechar
</button>
</div>
</div>
<div className="p-6 max-h-[70vh] overflow-y-auto">
<div className="max-w-[21cm] mx-auto bg-white shadow-lg p-8">
<div className="text-center mb-8 pb-4 border-b">
<h2 className="text-xl font-bold">
{documento?.escritorio?.nome || 'Nome do Escritório'}
</h2>
</div>
<div
className="prose max-w-none"
dangerouslySetInnerHTML={{ __html: documento?.conteudo }}
/>
<div className="text-center mt-8 pt-4 border-t text-sm text-gray-600">
{documento?.escritorio?.endereco}<br/>
{documento?.escritorio?.telefone} | {documento?.escritorio?.email}
</div>
</div>
</div>
</Dialog.Panel>
</div>
</Dialog>
);
}
@@ -0,0 +1,245 @@
import { useState, useEffect, useCallback } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { DocumentIcon, PlusIcon, TrashIcon, EyeIcon, CogIcon } from '@heroicons/react/24/outline';
import { DocumentoEditor } from './DocumentoEditor';
import { api } from '../lib/api';
import { toast } from 'react-toastify';
import { useAuth } from '../contexts/AuthContext';
import { Dialog } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { DocumentoViewer } from './DocumentoViewer';
export function DocumentosPage() {
const [isEditorOpen, setIsEditorOpen] = useState(false);
const [documentos, setDocumentos] = useState([]);
const [modelos, setModelos] = useState([]);
const [tabValue, setTabValue] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const { processoId, atendimentoId } = useParams();
const navigate = useNavigate();
const { user, signOut } = useAuth();
const [documentoVisualizado, setDocumentoVisualizado] = useState(null);
const [isVisualizadorOpen, setVisualizadorOpen] = useState(false);
const loadData = useCallback(async () => {
if (!user) {
navigate('/login');
return;
}
try {
setIsLoading(true);
await Promise.all([loadDocumentos(), loadModelos()]);
} catch (error) {
console.error('Erro ao carregar dados:', error);
if (error?.response?.status === 401) {
signOut(); // Usar a função do AuthContext
navigate('/login');
} else {
toast.error('Erro ao carregar dados');
}
} finally {
setIsLoading(false);
}
}, [user, navigate, signOut]); // Adicionar signOut nas dependências
useEffect(() => {
loadData();
}, [loadData, processoId, atendimentoId]);
const loadDocumentos = async () => {
const params = {};
if (processoId) params.processoId = processoId;
if (atendimentoId) params.atendimentoId = atendimentoId;
const response = await api.get('/documentos', { params });
setDocumentos(response.data);
};
const loadModelos = async () => {
const response = await api.get('/documentos-modelos');
setModelos(response.data);
};
const handleSave = async (data) => {
try {
if (processoId) data.processoId = processoId;
if (atendimentoId) data.atendimentoId = atendimentoId;
await api.post('/documentos', data);
toast.success('Documento salvo com sucesso');
await loadDocumentos();
setIsEditorOpen(false);
} catch (error) {
console.error('Erro ao salvar documento:', error);
if (error?.response?.status === 401) {
toast.error('Sessão expirada. Por favor, faça login novamente.');
} else {
toast.error('Erro ao salvar documento');
}
}
};
const handleDeleteDocumento = async (id) => {
try {
await api.delete(`/documentos/${id}`);
toast.success('Documento excluído com sucesso');
await loadDocumentos();
} catch (error) {
console.error('Erro ao excluir documento:', error);
if (error?.response?.status === 401) {
toast.error('Sessão expirada. Por favor, faça login novamente.');
} else {
toast.error('Erro ao excluir documento');
}
}
};
const handleVisualizar = async (documento) => {
try {
const configRes = await api.get('/documentos/configuracoes');
setDocumentoVisualizado({
...documento,
escritorio: configRes.data
});
setVisualizadorOpen(true);
} catch (error) {
console.error('Erro ao visualizar documento:', error);
toast.error('Erro ao visualizar documento');
}
};
if (isLoading) {
return <div>Carregando...</div>;
}
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Documentos</h1>
<div className="flex gap-2">
<button
onClick={() => setIsEditorOpen(true)}
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
>
<PlusIcon className="w-5 h-5 mr-2" />
Novo Documento
</button>
{user?.perfil === 'ADVOGADO_ADMIN' && (
<>
<button
onClick={() => navigate('/documentos/templates')}
className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors"
>
<DocumentIcon className="w-5 h-5 mr-2" />
Gerenciar Templates
</button>
<button
onClick={() => navigate('/documentos/configuracoes')}
className="inline-flex items-center px-4 py-2 border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 transition-colors"
>
<CogIcon className="w-5 h-5 mr-2" />
Configurar Timbrado
</button>
</>
)}
</div>
</div>
<div className="border-b border-gray-200">
<nav className="flex -mb-px">
<button
onClick={() => setTabValue(0)}
className={`px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${
tabValue === 0
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Documentos
</button>
<button
onClick={() => setTabValue(1)}
className={`ml-8 px-4 py-2 font-medium text-sm border-b-2 transition-colors duration-200 ${
tabValue === 1
? 'border-blue-500 text-blue-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
>
Modelos
</button>
</nav>
</div>
{tabValue === 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{documentos.map((doc) => (
<div key={doc.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="p-4">
<div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{doc.nome}</h3>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400">
Criado em: {new Date(doc.criadoEm).toLocaleDateString()}
</p>
<div className="mt-4 flex gap-2">
<button
onClick={() => handleVisualizar(doc)}
className="inline-flex items-center px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50"
>
<EyeIcon className="w-4 h-4 mr-1" />
Visualizar
</button>
<button
onClick={() => handleDeleteDocumento(doc.id)}
className="inline-flex items-center px-3 py-1.5 text-sm text-red-600 dark:text-red-400 border border-red-200 dark:border-red-900 rounded-md hover:bg-red-50 dark:hover:bg-red-900/20"
>
<TrashIcon className="w-4 h-4 mr-1" />
Excluir
</button>
</div>
</div>
</div>
))}
</div>
)}
{tabValue === 1 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{modelos.map((modelo) => (
<div key={modelo.id} className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="p-4">
<div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{modelo.nome}</h3>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{modelo.descricao}</p>
<button
className="w-full inline-flex items-center justify-center px-4 py-2 text-sm border border-blue-600 text-blue-600 rounded-md hover:bg-blue-50 dark:hover:bg-blue-900/20"
>
Usar Modelo
</button>
</div>
</div>
))}
</div>
)}
<DocumentoEditor
isOpen={isEditorOpen}
onClose={() => setIsEditorOpen(false)}
onSave={handleSave}
processoId={processoId}
atendimentoId={atendimentoId}
/>
<DocumentoViewer
documento={documentoVisualizado}
isOpen={isVisualizadorOpen}
onClose={() => setVisualizadorOpen(false)}
/>
</div>
);
}
@@ -0,0 +1,282 @@
import { useState, useEffect } from 'react';
import { api } from '../../lib/api';
import { Editor } from '@tinymce/tinymce-react';
import { toast } from 'react-toastify';
import { Dialog } from '@headlessui/react';
import { DocumentIcon, PencilIcon, TrashIcon, PlusIcon } from '@heroicons/react/24/outline';
import { DOCUMENT_VARIABLES } from '../../utils/documentVariables';
export function GerenciarTemplates() {
const [templates, setTemplates] = useState([]);
const [editingTemplate, setEditingTemplate] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditorModalOpen, setEditorModalOpen] = useState(false);
const [formData, setFormData] = useState({
nome: '',
tipo: 'OUTROS',
descricao: '',
conteudo: '',
variaveis: []
});
const loadTemplates = async () => {
try {
const response = await api.get('/documentos-modelos');
setTemplates(response.data);
} catch (error) {
toast.error('Erro ao carregar templates');
}
};
useEffect(() => {
loadTemplates();
}, []);
const handleSaveTemplate = async (data) => {
try {
if (editingTemplate) {
await api.put(`/documentos-modelos/${editingTemplate.id}`, data);
toast.success('Template atualizado com sucesso');
} else {
await api.post('/documentos-modelos', data);
toast.success('Template criado com sucesso');
}
loadTemplates();
setIsModalOpen(false);
} catch (error) {
toast.error('Erro ao salvar template');
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Verifica se está editando ou criando novo
if (editingTemplate) {
await api.put(`/documentos-modelos/${editingTemplate.id}`, {
...formData,
id: editingTemplate.id // Garantir que o ID seja enviado
});
toast.success('Template atualizado com sucesso');
} else {
await api.post('/documentos-modelos', formData);
toast.success('Template criado com sucesso');
}
setEditorModalOpen(false);
setEditingTemplate(null);
await loadTemplates(); // Recarregar a lista após salvar
} catch (error) {
console.error('Erro ao salvar template:', error);
toast.error('Erro ao salvar template');
}
};
const handleDelete = async (templateId: string) => {
if (!window.confirm('Tem certeza que deseja excluir este template?')) {
return;
}
try {
await api.delete(`/documentos-modelos/${templateId}`);
toast.success('Template excluído com sucesso');
loadTemplates();
} catch (error) {
console.error('Erro ao excluir template:', error);
toast.error('Erro ao excluir template');
}
};
const insertVariable = (variable: string) => {
const editor = window.tinymce.activeEditor;
if (editor) {
editor.insertContent(variable);
}
};
const handleEdit = (template) => {
setEditingTemplate(template); // Guardar o template completo
setFormData({
...template,
id: template.id // Garantir que o ID seja mantido
});
setEditorModalOpen(true);
};
return (
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold">Gerenciar Templates</h1>
<button
onClick={() => {
setFormData({
nome: '',
tipo: 'OUTROS',
descricao: '',
conteudo: '',
variaveis: []
});
setEditorModalOpen(true);
}}
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
<PlusIcon className="w-5 h-5 mr-2" />
Novo Template
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{templates.map((template) => (
<div key={template.id} className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<div className="flex items-center mb-3">
<DocumentIcon className="w-6 h-6 text-gray-500 mr-2" />
<h3 className="font-semibold">{template.nome}</h3>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">{template.descricao}</p>
<div className="flex gap-2">
<button
onClick={() => handleEdit(template)}
className="inline-flex items-center px-3 py-1.5 text-sm text-blue-600 border border-blue-200 rounded-md hover:bg-blue-50"
>
<PencilIcon className="w-4 h-4 mr-1" />
Editar
</button>
<button
onClick={() => handleDelete(template.id)}
className="inline-flex items-center px-3 py-1.5 text-sm text-red-600 border border-red-200 rounded-md hover:bg-red-50"
>
<TrashIcon className="w-4 h-4 mr-1" />
Excluir
</button>
</div>
</div>
))}
</div>
{/* Modal de Edição */}
<Dialog open={isEditorModalOpen} onClose={() => setEditorModalOpen(false)} className="relative z-50">
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4 overflow-y-auto">
<Dialog.Panel className="w-full max-w-4xl bg-white rounded-lg shadow-xl my-4">
<div className="max-h-[90vh] overflow-y-auto">
<form onSubmit={handleSubmit} className="p-6">
<h2 className="text-xl font-bold mb-4">
{editingTemplate ? 'Editar Template' : 'Novo Template'}
</h2>
<div className="space-y-4">
<div>
<label className="block mb-1">Nome</label>
<input
type="text"
value={formData.nome}
onChange={(e) => setFormData({...formData, nome: e.target.value})}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="block mb-1">Tipo</label>
<select
value={formData.tipo}
onChange={(e) => setFormData({...formData, tipo: e.target.value})}
className="w-full p-2 border rounded"
>
<option value="PETICAO">Petição</option>
<option value="CONTRATO">Contrato</option>
<option value="PROCURACAO">Procuração</option>
<option value="DECLARACAO">Declaração</option>
<option value="OUTROS">Outros</option>
</select>
</div>
<div>
<label className="block mb-1">Descrição</label>
<textarea
value={formData.descricao}
onChange={(e) => setFormData({...formData, descricao: e.target.value})}
className="w-full p-2 border rounded"
rows={3}
/>
</div>
<div className="mb-4">
<label className="block mb-1">Variáveis Disponíveis</label>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 p-2 border rounded bg-gray-50">
{Object.entries(DOCUMENT_VARIABLES).map(([category, variables]) => (
<div key={category} className="space-y-1">
<h4 className="font-bold text-sm text-gray-700">{category}</h4>
{Object.entries(variables).map(([name, variable]) => (
<button
key={name}
type="button"
onClick={() => insertVariable(variable)}
className="text-xs text-blue-600 hover:text-blue-800 block"
>
{name.replace(/_/g, ' ')}
</button>
))}
</div>
))}
</div>
</div>
<div>
<label className="block mb-1">Conteúdo</label>
<Editor
apiKey={import.meta.env.VITE_TINYMCE_API_KEY}
value={formData.conteudo}
onEditorChange={(content) => setFormData({...formData, conteudo: content})}
init={{
height: 400,
menubar: true,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount'
],
toolbar: 'variables | undo redo | formatselect | bold italic | alignleft aligncenter alignright',
setup: (editor) => {
editor.ui.registry.addMenuButton('variables', {
text: 'Variáveis',
fetch: (callback) => {
const items = Object.entries(DOCUMENT_VARIABLES).flatMap(([category, vars]) =>
Object.entries(vars).map(([name, value]) => ({
type: 'menuitem',
text: `${category} - ${name.replace(/_/g, ' ')}`,
onAction: () => editor.insertContent(value)
}))
);
callback(items);
}
});
}
}}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button
type="button"
onClick={() => setEditorModalOpen(false)}
className="px-4 py-2 text-gray-600 border rounded-md hover:bg-gray-50"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
>
Salvar
</button>
</div>
</form>
</div>
</Dialog.Panel>
</div>
</Dialog>
</div>
);
}
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { DocumentoService } from '../../services/DocumentoService';
import { TIPOS_DOCUMENTO } from '../../utils/documentTemplates';
export function NovoDocumento() {
const [tipo, setTipo] = useState(TIPOS_DOCUMENTO.PROCURACAO);
const handleGerar = async () => {
// 1. Buscar dados necessários
const cliente = await api.get(`/clientes/${clienteId}`);
const advogado = await api.get('/usuarios/me');
// 2. Preparar variáveis
const variaveis = {
cliente: cliente.data,
advogado: advogado.data,
cidade: 'São Paulo',
data: new Date().toLocaleDateString()
};
// 3. Buscar configurações do escritório
const config = await api.get('/configuracoes');
// 4. Gerar documento
const documento = DocumentoService.gerarDocumento({
tipo,
variaveis,
config: config.data
});
// 5. Salvar ou exibir
console.log(documento);
};
return (
<div>
<select value={tipo} onChange={e => setTipo(e.target.value)}>
<option value={TIPOS_DOCUMENTO.PROCURACAO}>Procuração</option>
<option value={TIPOS_DOCUMENTO.CONTRATO}>Contrato</option>
{/* ... */}
</select>
<button onClick={handleGerar}>
Gerar Documento
</button>
</div>
);
}