647 lines
26 KiB
TypeScript
647 lines
26 KiB
TypeScript
import React, { useState, useEffect, lazy, Suspense } from "react";
|
|
import { useParams, useNavigate } from "react-router-dom";
|
|
import { useProcessos } from "../../hooks/useProcessos";
|
|
import api from "../../services/api"; // Adicionar esta importação
|
|
import { Tab, Dialog } from "@headlessui/react";
|
|
import { TrashIcon } from "@heroicons/react/24/outline";
|
|
import {
|
|
ArrowLeftIcon,
|
|
ClockIcon,
|
|
DocumentTextIcon,
|
|
UserGroupIcon,
|
|
BanknotesIcon, // Alterado de BalanceIcon para BanknotesIcon
|
|
CalendarIcon,
|
|
PencilIcon,
|
|
ExclamationTriangleIcon,
|
|
PlusIcon,
|
|
} from "@heroicons/react/24/outline";
|
|
import { ProcessoModal } from "./ProcessoModal.tsx";
|
|
import { formatNumeroProcesso, formatCPF } from "../../utils/formatters";
|
|
import { ParteProcessoModal } from "./ParteProcessoModal";
|
|
import { usePartesProcesso } from "../../hooks/usePartesProcesso";
|
|
import { useAudiencias } from "../../hooks/useAudiencias";
|
|
import { AudienciaModal } from "./AudienciaModal";
|
|
import AbaFinanceiro from "./AbaFinanceiro";
|
|
import { ProcessoProvider } from "../../context/ProcessoContext";
|
|
|
|
// Adicionar lazy import para AbaMovimentacoes
|
|
const AbaMovimentacoes = lazy(() => import("./AbaMovimentacoes"));
|
|
|
|
function classNames(...classes: string[]) {
|
|
return classes.filter(Boolean).join(" ");
|
|
}
|
|
|
|
const tabs = [
|
|
{ id: "geral", name: "Geral", icon: DocumentTextIcon },
|
|
{ id: "partes", name: "Partes", icon: UserGroupIcon },
|
|
{ id: "movimentacoes", name: "Movimentações", icon: ClockIcon },
|
|
{ id: "audiencias", name: "Audiências", icon: CalendarIcon },
|
|
{ id: "financeiro", name: "Financeiro", icon: BanknotesIcon }, // Atualizado aqui também
|
|
];
|
|
|
|
export const ProcessoDetalhes: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { getProcesso, isLoading, deleteProcesso } = useProcessos();
|
|
const [processo, setProcesso] = useState<Processo | null>(null);
|
|
const [selectedTab, setSelectedTab] = useState(0);
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
const [editingParte, setEditingParte] = useState<any>(null);
|
|
const [parteModalConfig, setParteModalConfig] = useState<{
|
|
isOpen: boolean;
|
|
tipo: "AUTOR" | "REU" | null;
|
|
}>({ isOpen: false, tipo: null });
|
|
const [isAudienciaModalOpen, setIsAudienciaModalOpen] = useState(false);
|
|
const [editingAudiencia, setEditingAudiencia] = useState<any>(null);
|
|
const [isFinalizacao, setIsFinalizacao] = useState(false); // Movido para cá
|
|
|
|
// Carregar processo com partes
|
|
useEffect(() => {
|
|
const loadProcesso = async () => {
|
|
if (!id) return;
|
|
try {
|
|
const data = await getProcesso(id);
|
|
// Só atualizar se os dados forem diferentes
|
|
if (JSON.stringify(data) !== JSON.stringify(processo)) {
|
|
setProcesso(data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar processo:", error);
|
|
}
|
|
};
|
|
|
|
loadProcesso();
|
|
}, [id, getProcesso]); // Remover processo da dependency list
|
|
|
|
const { addParte, updateParte, deleteParte } = usePartesProcesso(id!);
|
|
|
|
const handleSaveParte = async (data: any) => {
|
|
try {
|
|
if (editingParte) {
|
|
await updateParte({ id: editingParte.id, data });
|
|
} else {
|
|
await addParte(data);
|
|
}
|
|
setParteModalConfig({ isOpen: false, tipo: null });
|
|
setEditingParte(null);
|
|
// Recarregar processo após salvar
|
|
const updatedProcesso = await getProcesso(id!);
|
|
setProcesso(updatedProcesso);
|
|
} catch (error) {
|
|
console.error("Erro ao salvar parte:", error);
|
|
}
|
|
};
|
|
|
|
const handleDeleteParte = async (parteId: string) => {
|
|
if (window.confirm("Tem certeza que deseja remover esta parte?")) {
|
|
try {
|
|
await deleteParte(parteId);
|
|
} catch (error) {
|
|
console.error("Erro ao excluir parte:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const { addAudiencia, updateAudiencia, deleteAudiencia } = useAudiencias(id!);
|
|
|
|
const handleSaveAudiencia = async (data: any) => {
|
|
try {
|
|
if (editingAudiencia) {
|
|
await updateAudiencia({ id: editingAudiencia.id, data });
|
|
} else {
|
|
await addAudiencia(data);
|
|
}
|
|
setIsAudienciaModalOpen(false);
|
|
setEditingAudiencia(null);
|
|
// Recarregar processo após salvar
|
|
const updatedProcesso = await getProcesso(id!);
|
|
setProcesso(updatedProcesso);
|
|
} catch (error) {
|
|
console.error("Erro ao salvar audiência:", error);
|
|
}
|
|
};
|
|
|
|
const handleDeleteAudiencia = async (audienciaId: string) => {
|
|
if (window.confirm("Tem certeza que deseja remover esta audiência?")) {
|
|
try {
|
|
await deleteAudiencia(audienciaId);
|
|
// Recarregar processo
|
|
const updatedProcesso = await getProcesso(id!);
|
|
setProcesso(updatedProcesso);
|
|
} catch (error) {
|
|
console.error("Erro ao excluir audiência:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isLoading) {
|
|
return <div className="animate-pulse">Carregando...</div>;
|
|
}
|
|
|
|
if (!processo) {
|
|
return <div>Processo não encontrado</div>;
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
try {
|
|
await deleteProcesso.mutateAsync(id!);
|
|
navigate("/processos");
|
|
} catch (error) {
|
|
console.error("Erro ao excluir processo:", error);
|
|
alert("Erro ao excluir processo");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-4">
|
|
<button
|
|
onClick={() => navigate("/processos")}
|
|
className="p-2 hover:bg-gray-100 rounded-full"
|
|
>
|
|
<ArrowLeftIcon className="h-5 w-5 text-gray-500" />
|
|
</button>
|
|
<div>
|
|
<h1 className="text-xl font-semibold text-gray-900">
|
|
{formatNumeroProcesso(processo.numero)}
|
|
</h1>
|
|
<p className="text-sm text-gray-500">{processo.natureza}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
{/* Status Badge */}
|
|
<span
|
|
className={classNames(
|
|
"px-3 py-1 rounded-full text-sm font-medium",
|
|
processo.status === "INICIAL"
|
|
? "bg-yellow-100 text-yellow-800"
|
|
: processo.status === "EM_ANDAMENTO"
|
|
? "bg-blue-100 text-blue-800"
|
|
: processo.status === "SUSPENSO"
|
|
? "bg-gray-100 text-gray-800"
|
|
: processo.status === "CONCLUSO"
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-red-100 text-red-800",
|
|
)}
|
|
>
|
|
{processo.status}
|
|
</span>
|
|
|
|
{/* Adicionar o botão de resultado aqui */}
|
|
<select
|
|
value={processo.resultado || 'NAO_JULGADO'}
|
|
onChange={async (e) => {
|
|
try {
|
|
await api.patch(`/processos/${processo.id}/resultado`, {
|
|
resultado: e.target.value
|
|
});
|
|
// Recarregar dados após atualização
|
|
const updated = await getProcesso(processo.id);
|
|
setProcesso(updated);
|
|
} catch (error) {
|
|
console.error('Erro ao atualizar resultado:', error);
|
|
alert('Erro ao atualizar resultado do processo');
|
|
}
|
|
}}
|
|
className="rounded-lg border-gray-300 text-sm"
|
|
>
|
|
<option value="NAO_JULGADO">Não Julgado</option>
|
|
<option value="PROCEDENTE">Procedente</option>
|
|
<option value="PARCIALMENTE_PROCEDENTE">Parcialmente Procedente</option>
|
|
<option value="IMPROCEDENTE">Improcedente</option>
|
|
<option value="SEM_ANALISE_MERITO">Sem Análise de Mérito</option>
|
|
</select>
|
|
</div>
|
|
{/* Adicionar botão de edição */}
|
|
<button
|
|
onClick={() => setIsEditModalOpen(true)}
|
|
className="flex items-center px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
|
|
>
|
|
<PencilIcon className="h-4 w-4 mr-2" />
|
|
Editar Processo
|
|
</button>
|
|
<button
|
|
onClick={() => setIsDeleteModalOpen(true)}
|
|
className="flex items-center px-4 py-2 text-sm text-red-600 hover:bg-red-50 rounded-lg"
|
|
>
|
|
<TrashIcon className="h-4 w-4 mr-2" />
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
{processo && (
|
|
<ProcessoProvider processoId={processo.id}>
|
|
<Tab.Group selectedIndex={selectedTab} onChange={setSelectedTab}>
|
|
<Tab.List className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
|
|
{tabs.map((tab) => (
|
|
<Tab
|
|
key={tab.id}
|
|
className={({ selected }) =>
|
|
classNames(
|
|
"w-full rounded-lg py-2.5 text-sm font-medium leading-5",
|
|
"flex items-center justify-center",
|
|
selected
|
|
? "bg-white text-blue-700 shadow"
|
|
: "text-gray-600 hover:bg-white/[0.12] hover:text-blue-600",
|
|
)
|
|
}
|
|
>
|
|
<tab.icon className="h-4 w-4 mr-2" />
|
|
{tab.name}
|
|
</Tab>
|
|
))}
|
|
</Tab.List>
|
|
|
|
<Tab.Panels className="mt-2">
|
|
{/* Informações Gerais */}
|
|
<Tab.Panel className="bg-white rounded-xl p-6">
|
|
<dl className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<dt className="text-sm font-medium text-gray-500">
|
|
Número do Processo
|
|
</dt>
|
|
<dd className="mt-1 text-sm text-gray-900">
|
|
{formatNumeroProcesso(processo.numero)}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="text-sm font-medium text-gray-500">
|
|
Natureza
|
|
</dt>
|
|
<dd className="mt-1 text-sm text-gray-900">
|
|
{processo.natureza}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="text-sm font-medium text-gray-500">
|
|
Órgão Julgador
|
|
</dt>
|
|
<dd className="mt-1 text-sm text-gray-900">
|
|
{processo.orgaoJulgador}
|
|
</dd>
|
|
</div>
|
|
<div>
|
|
<dt className="text-sm font-medium text-gray-500">
|
|
Valor da Causa
|
|
</dt>
|
|
<dd className="mt-1 text-sm text-gray-900">
|
|
{new Intl.NumberFormat("pt-BR", {
|
|
style: "currency",
|
|
currency: "BRL",
|
|
}).format(processo.valorCausa || 0)}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</Tab.Panel>
|
|
|
|
{/* Partes do Processo */}
|
|
<Tab.Panel className="bg-white rounded-xl p-6">
|
|
<div className="space-y-8">
|
|
{/* Autores */}
|
|
<div>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Autores
|
|
</h3>
|
|
<button
|
|
onClick={() => {
|
|
setEditingParte(null);
|
|
setParteModalConfig({ isOpen: true, tipo: "AUTOR" });
|
|
}}
|
|
className="text-sm text-blue-600 hover:text-blue-700"
|
|
>
|
|
+ Adicionar Autor
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{processo.partes
|
|
?.filter((p) => p.tipo === "AUTOR")
|
|
.map((parte) => (
|
|
<div
|
|
key={parte.id}
|
|
className="flex justify-between items-center p-4 bg-gray-50 rounded-lg"
|
|
>
|
|
<div>
|
|
<h4 className="font-medium">{parte.nome}</h4>
|
|
{parte.cpfCnpj && (
|
|
<p className="text-sm text-gray-500">
|
|
{formatCPF(parte.cpfCnpj)}
|
|
</p>
|
|
)}
|
|
{parte.observacoes && (
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
{parte.observacoes}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<button
|
|
onClick={() => {
|
|
setEditingParte(parte);
|
|
setParteModalConfig({
|
|
isOpen: true,
|
|
tipo: "AUTOR",
|
|
});
|
|
}}
|
|
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
|
>
|
|
<PencilIcon className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteParte(parte.id)}
|
|
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Réus */}
|
|
<div>
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Réus
|
|
</h3>
|
|
<button
|
|
onClick={() => {
|
|
setEditingParte(null);
|
|
setParteModalConfig({ isOpen: true, tipo: "REU" });
|
|
}}
|
|
className="text-sm text-blue-600 hover:text-blue-700"
|
|
>
|
|
+ Adicionar Réu
|
|
</button>
|
|
</div>
|
|
<div className="space-y-4">
|
|
{processo.partes
|
|
?.filter((p) => p.tipo === "REU")
|
|
.map((parte) => (
|
|
<div
|
|
key={parte.id}
|
|
className="flex justify-between items-center p-4 bg-gray-50 rounded-lg"
|
|
>
|
|
<div>
|
|
<h4 className="font-medium">{parte.nome}</h4>
|
|
{parte.cpfCnpj && (
|
|
<p className="text-sm text-gray-500">
|
|
{formatCPF(parte.cpfCnpj)}
|
|
</p>
|
|
)}
|
|
{parte.observacoes && (
|
|
<p className="text-sm text-gray-600 mt-1">
|
|
{parte.observacoes}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex space-x-2">
|
|
<button
|
|
onClick={() => {
|
|
setEditingParte(parte);
|
|
setParteModalConfig({
|
|
isOpen: true,
|
|
tipo: "REU",
|
|
});
|
|
}}
|
|
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
|
>
|
|
<PencilIcon className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteParte(parte.id)}
|
|
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<ParteProcessoModal
|
|
isOpen={parteModalConfig.isOpen}
|
|
onClose={() =>
|
|
setParteModalConfig({ isOpen: false, tipo: null })
|
|
}
|
|
onSave={handleSaveParte}
|
|
tipo={parteModalConfig.tipo}
|
|
editingParte={editingParte}
|
|
/>
|
|
</Tab.Panel>
|
|
|
|
{/* Movimentações */}
|
|
<Tab.Panel className="bg-white rounded-xl">
|
|
<Suspense
|
|
fallback={
|
|
<div className="flex justify-center p-8">Carregando...</div>
|
|
}
|
|
>
|
|
<AbaMovimentacoes />
|
|
</Suspense>
|
|
</Tab.Panel>
|
|
|
|
{/* Audiências */}
|
|
<Tab.Panel className="bg-white rounded-xl p-6">
|
|
<div className="space-y-4">
|
|
{/* Header com botão de adicionar */}
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Audiências
|
|
</h3>
|
|
<button
|
|
onClick={() => {
|
|
setEditingAudiencia(null);
|
|
setIsAudienciaModalOpen(true);
|
|
}}
|
|
className="flex items-center px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
|
|
>
|
|
<PlusIcon className="h-4 w-4 mr-2" />
|
|
Nova Audiência
|
|
</button>
|
|
</div>
|
|
|
|
{/* Lista de Audiências */}
|
|
{processo.audiencias && processo.audiencias.length > 0 ? (
|
|
<div className="divide-y divide-gray-200">
|
|
{processo.audiencias.map((audiencia) => (
|
|
<div key={audiencia.id} className="py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h4 className="text-lg font-medium text-gray-900">
|
|
{audiencia.tipo}
|
|
</h4>
|
|
<div className="mt-1 text-sm text-gray-500">
|
|
<p>
|
|
Data:{" "}
|
|
{new Date(audiencia.data).toLocaleString()}
|
|
</p>
|
|
<p>Local: {audiencia.local}</p>
|
|
{audiencia.modalidade === "VIRTUAL" && (
|
|
<p>
|
|
Link:{" "}
|
|
<a
|
|
href={audiencia.linkVirtual}
|
|
className="text-blue-600 hover:underline"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Acessar
|
|
</a>
|
|
</p>
|
|
)}
|
|
{audiencia.relatorio && (
|
|
<div className="mt-2 p-3 bg-gray-50 rounded-lg">
|
|
<p className="font-medium text-sm">
|
|
Relatório:
|
|
</p>
|
|
<p className="text-sm mt-1">
|
|
{audiencia.relatorio}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-1">
|
|
Finalizada em:{" "}
|
|
{new Date(
|
|
audiencia.dataFinalizacao,
|
|
).toLocaleString()}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
{audiencia.status === "AGENDADA" && (
|
|
<button
|
|
onClick={() => {
|
|
setEditingAudiencia(audiencia);
|
|
setIsAudienciaModalOpen(true);
|
|
setIsFinalizacao(true);
|
|
}}
|
|
className="px-3 py-1 text-sm text-green-600 hover:bg-green-50 rounded-lg border border-green-600"
|
|
>
|
|
Finalizar
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => {
|
|
setEditingAudiencia(audiencia);
|
|
setIsAudienciaModalOpen(true);
|
|
}}
|
|
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
|
|
>
|
|
<PencilIcon className="h-4 w-4" />
|
|
</button>
|
|
<button
|
|
onClick={() =>
|
|
handleDeleteAudiencia(audiencia.id)
|
|
}
|
|
className="p-1 text-red-600 hover:bg-red-50 rounded"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-500 text-center py-4">
|
|
Nenhuma audiência agendada
|
|
</p>
|
|
)}
|
|
</div>
|
|
</Tab.Panel>
|
|
|
|
{/* Financeiro */}
|
|
<Tab.Panel className="bg-white rounded-xl p-6">
|
|
<ProcessoProvider processoId={processo?.id}>
|
|
<AbaFinanceiro />
|
|
</ProcessoProvider>
|
|
</Tab.Panel>
|
|
</Tab.Panels>
|
|
</Tab.Group>
|
|
</ProcessoProvider>
|
|
)}
|
|
|
|
{/* Modal de Edição */}
|
|
<ProcessoModal
|
|
isOpen={isEditModalOpen}
|
|
processo={processo}
|
|
onClose={() => setIsEditModalOpen(false)}
|
|
onSuccess={() => {
|
|
setIsEditModalOpen(false);
|
|
// Recarregar dados após edição
|
|
window.location.reload();
|
|
}}
|
|
/>
|
|
|
|
{/* Modal de Confirmação de Exclusão */}
|
|
<Dialog
|
|
open={isDeleteModalOpen}
|
|
onClose={() => setIsDeleteModalOpen(false)}
|
|
className="relative z-50"
|
|
>
|
|
<div
|
|
className="fixed inset-0 bg-black/30 backdrop-blur-sm"
|
|
aria-hidden="true"
|
|
/>
|
|
|
|
<div className="fixed inset-0 flex items-center justify-center p-4">
|
|
<Dialog.Panel className="mx-auto max-w-sm rounded-lg bg-white p-6 shadow-xl">
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex-shrink-0">
|
|
<ExclamationTriangleIcon className="h-10 w-10 text-red-500" />
|
|
</div>
|
|
<div>
|
|
<Dialog.Title className="text-lg font-medium text-gray-900">
|
|
Confirmar exclusão
|
|
</Dialog.Title>
|
|
<Dialog.Description className="mt-2 text-sm text-gray-500">
|
|
Tem certeza que deseja excluir este processo? Esta ação não
|
|
poderá ser desfeita.
|
|
</Dialog.Description>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 flex justify-end space-x-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsDeleteModalOpen(false)}
|
|
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleDelete}
|
|
className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700"
|
|
>
|
|
Excluir
|
|
</button>
|
|
</div>
|
|
</Dialog.Panel>
|
|
</div>
|
|
</Dialog>
|
|
|
|
{/* Adicionar Modal de Audiência */}
|
|
<AudienciaModal
|
|
isOpen={isAudienciaModalOpen}
|
|
onClose={() => {
|
|
setIsAudienciaModalOpen(false);
|
|
setEditingAudiencia(null);
|
|
setIsFinalizacao(false);
|
|
}}
|
|
onSave={handleSaveAudiencia}
|
|
editingAudiencia={editingAudiencia}
|
|
isFinalizacao={isFinalizacao}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|