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

+554
View File
@@ -0,0 +1,554 @@
import { useState, lazy, Suspense, useEffect } from "react";
import { Tab } from "@headlessui/react";
import { PlusIcon, PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
import { formatCurrency, formatDate } from "../../utils/format";
import { useCustasProcessuais } from "../../hooks/useCustasProcessuais";
import { useHonorarios } from "../../hooks/useHonorarios";
import { useProcessoContext } from "../../context/ProcessoContext";
import api from "../../services/api"; // Adicionar esta importação
// Corrigir importações lazy
const DialogCustaProcessual = lazy(() =>
import("./dialogs/DialogCustaProcessual").then((module) => ({
default: module.default,
})),
);
const DialogHonorario = lazy(() =>
import("./dialogs/DialogHonorario").then((module) => ({
default: module.default,
})),
);
const DialogParcela = lazy(() =>
import("./dialogs/DialogParcela").then((module) => ({
default: module.DialogParcela, // Usar .DialogParcela em vez de .default
})),
);
const getParcelaStatus = (parcela: any) => {
if (parcela.status === "PAGO") return "PAGO";
if (parcela.valorPago && parcela.valorPago > 0) return "PARCIALMENTE_PAGO";
return parcela.status;
};
export default function AbaFinanceiro() {
const { processo, refreshProcesso } = useProcessoContext();
// Verificar se processo existe antes de usar
useEffect(() => {
if (!processo) {
refreshProcesso();
}
}, [processo, refreshProcesso]);
// Restante do estado
const [tabValue, setTabValue] = useState(0);
const [custaDialogOpen, setCustaDialogOpen] = useState(false);
const [honorarioDialogOpen, setHonorarioDialogOpen] = useState(false);
const [editingCusta, setEditingCusta] = useState(null);
const [editingHonorario, setEditingHonorario] = useState(null);
const [confirmDelete, setConfirmDelete] = useState({
show: false,
type: null,
id: null,
});
const [editingParcela, setEditingParcela] = useState<any>(null);
const [parcelaDialogOpen, setParcelaDialogOpen] = useState(false);
const [parcelaTipo, setParcelaTipo] = useState<"honorario" | "custa">(
"honorario",
);
// Inicializar hooks com ID do processo
const { addCusta, updateCusta, deleteCusta } = useCustasProcessuais(
processo?.id,
);
const { addHonorario, updateHonorario, deleteHonorario } = useHonorarios(
processo?.id,
);
// Se não houver processo, mostrar loading
if (!processo) {
return <div className="flex justify-center p-8">Carregando...</div>;
}
const handleSaveCusta = async (data: any) => {
try {
if (editingCusta) {
await updateCusta({ id: editingCusta.id, data });
} else {
await addCusta(data);
}
setCustaDialogOpen(false);
setEditingCusta(null);
await refreshProcesso(); // Use refreshProcesso instead of window.location.reload()
} catch (error) {
console.error("Erro ao salvar custa:", error);
}
};
const handleSaveHonorario = async (data: any) => {
try {
if (editingHonorario) {
await updateHonorario({ id: editingHonorario.id, data });
} else {
await addHonorario(data);
}
setHonorarioDialogOpen(false);
setEditingHonorario(null);
await refreshProcesso(); // Use refreshProcesso instead of window.location.reload()
} catch (error) {
console.error("Erro ao salvar honorário:", error);
}
};
const handleDelete = async () => {
try {
if (confirmDelete.type === "custa") {
await deleteCusta(confirmDelete.id);
} else if (confirmDelete.type === "honorario") {
await deleteHonorario(confirmDelete.id);
}
setConfirmDelete({ show: false, type: null, id: null });
await refreshProcesso(); // Use refreshProcesso instead of window.location.reload()
} catch (error) {
console.error("Erro ao excluir:", error);
}
};
const handleUpdateParcela = async (data: any) => {
try {
if (parcelaTipo === "honorario") {
// Remover /api do início pois já está configurado no baseURL do axios
await api.put(`/honorarios/parcelas/${editingParcela.id}`, {
status: data.status,
dataPagamento: data.dataPagamento,
valorPago: Number(data.valorPago),
comprovante: data.comprovante,
observacoes: data.observacoes,
});
}
setParcelaDialogOpen(false);
setEditingParcela(null);
await refreshProcesso();
} catch (error) {
console.error("Erro ao atualizar parcela:", error);
}
};
const getStatusBadgeClasses = (status: string) => {
const baseClasses = "px-2 py-1 text-xs font-medium rounded-full";
switch (status) {
case "PENDENTE":
return `${baseClasses} bg-yellow-100 text-yellow-800`;
case "PAGO":
return `${baseClasses} bg-green-100 text-green-800`;
case "VENCIDO":
return `${baseClasses} bg-red-100 text-red-800`;
case "CANCELADO":
return `${baseClasses} bg-gray-100 text-gray-800`;
default:
return `${baseClasses} bg-gray-100 text-gray-800`;
}
};
return (
<div className="py-4">
<Tab.Group>
<Tab.List className="flex space-x-1 rounded-xl bg-blue-900/20 p-1">
<Tab
className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium leading-5 ${
selected
? "bg-white text-blue-700 shadow"
: "text-gray-600 hover:bg-white/[0.12] hover:text-blue-600"
}`
}
>
Custas Processuais
</Tab>
<Tab
className={({ selected }) =>
`w-full rounded-lg py-2.5 text-sm font-medium leading-5 ${
selected
? "bg-white text-blue-700 shadow"
: "text-gray-600 hover:bg-white/[0.12] hover:text-blue-600"
}`
}
>
Honorários
</Tab>
</Tab.List>
<Tab.Panels className="mt-4">
<Tab.Panel className="bg-white rounded-xl p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-gray-900">
Custas Processuais
</h3>
<button
onClick={() => {
setEditingCusta(null);
setCustaDialogOpen(true);
}}
className="inline-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 Custa
</button>
</div>
{processo.custas?.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Tipo
</th>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Descrição
</th>
<th
scope="col"
className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Valor
</th>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Ações
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{processo.custas.map((custa) => (
<tr key={custa.id} className="hover:bg-gray-50">
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{custa.tipo.replace(/_/g, " ")}
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{custa.descricao}
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 text-right">
{formatCurrency(custa.valor)}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<span className={getStatusBadgeClasses(custa.status)}>
{custa.status}
</span>
</td>
<td className="px-4 py-2 whitespace-nowrap text-right">
<div className="flex justify-end space-x-2">
<button
onClick={() => {
setEditingCusta(custa);
setCustaDialogOpen(true);
}}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() =>
setConfirmDelete({
show: true,
type: "custa",
id: custa.id,
})
}
className="p-1 text-red-600 hover:bg-red-50 rounded"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="text-center py-8 text-gray-500">
Nenhuma custa processual registrada
</div>
)}
</Tab.Panel>
<Tab.Panel className="bg-white rounded-xl p-6">
<div className="flex justify-between items-center mb-6">
<h3 className="text-lg font-medium text-gray-900">Honorários</h3>
<button
onClick={() => {
setEditingHonorario(null);
setHonorarioDialogOpen(true);
}}
className="inline-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" />
Novo Honorário
</button>
</div>
{processo.honorarios?.map((honorario) => (
<div
key={honorario.id}
className="mb-6 p-4 bg-white rounded-lg shadow"
>
<div className="flex justify-between items-center">
<h4 className="text-lg font-medium text-gray-900">
{honorario.formaPagamento === "VISTA"
? "Pagamento à Vista"
: honorario.formaPagamento === "PARCELADO"
? `Parcelado em ${honorario.numeroParcelas}x`
: "Percentual da Causa"}
</h4>
<div className="flex items-center space-x-2">
<span className={getStatusBadgeClasses(honorario.status)}>
{honorario.status}
</span>
<button
onClick={() => {
setEditingHonorario(honorario);
setHonorarioDialogOpen(true);
}}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() =>
setConfirmDelete({
show: true,
type: "honorario",
id: honorario.id,
})
}
className="p-1 text-red-600 hover:bg-red-50 rounded"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</div>
<div className="mt-4">
<p className="text-sm text-gray-700">
{honorario.valorFixo
? `Valor: ${formatCurrency(honorario.valorFixo)}`
: `Percentual: ${honorario.percentualCausa}%`}
</p>
{honorario.formaPagamento === "PARCELADO" && (
<>
<p className="text-sm text-gray-700">
Entrada: {formatCurrency(honorario.valorEntrada)}
</p>
<p className="text-sm text-gray-700">
Parcelas: {honorario.numeroParcelas}x de{" "}
{formatCurrency(honorario.valorParcela)}
</p>
</>
)}
<p className="text-sm text-gray-500">
Vencimento: {formatDate(honorario.dataVencimento)}
</p>
</div>
{honorario.parcelas && (
<div className="mt-4 overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead>
<tr>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Parcela
</th>
<th
scope="col"
className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Valor
</th>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Vencimento
</th>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Ações
</th>
<th
scope="col"
className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Valor Pago
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{honorario.parcelas.map((parcela) => (
<tr key={parcela.id} className="hover:bg-gray-50">
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{parcela.numeroParcela}
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900 text-right">
{formatCurrency(parcela.valor)}
</td>
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-900">
{formatDate(parcela.vencimento)}
</td>
<td className="px-4 py-2 whitespace-nowrap">
<span
className={getStatusBadgeClasses(
parcela.status,
)}
>
{parcela.status}
</span>
</td>
<td className="px-4 py-2 whitespace-nowrap text-right">
<div className="flex space-x-2">
<button
onClick={() => {
setEditingParcela(parcela);
setParcelaTipo("honorario");
setParcelaDialogOpen(true);
}}
className="p-1 text-blue-600 hover:bg-blue-50 rounded"
>
<PencilIcon className="h-4 w-4" />
</button>
</div>
</td>
<td className="px-4 py-2 whitespace-nowrap text-xs text-gray-500">
{parcela.valorPago > 0 &&
parcela.valorPago < parcela.valor &&
`Pago: ${formatCurrency(parcela.valorPago)}`}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
))}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
{confirmDelete.show && (
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center">
<div className="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75"></div>
<div className="relative inline-block p-6 bg-white rounded-lg text-left shadow-xl">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0">
<TrashIcon className="h-6 w-6 text-red-600" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg font-medium text-gray-900">
Confirmar exclusão
</h3>
<div className="mt-2">
<p className="text-sm text-gray-500">
Tem certeza que deseja excluir este{" "}
{confirmDelete.type === "custa"
? "registro de custa"
: "honorário"}
? Esta ação não pode ser desfeita.
</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
onClick={handleDelete}
className="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 sm:ml-3 sm:w-auto sm:text-sm"
>
Excluir
</button>
<button
type="button"
onClick={() =>
setConfirmDelete({ show: false, type: null, id: null })
}
className="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 sm:mt-0 sm:w-auto sm:text-sm"
>
Cancelar
</button>
</div>
</div>
</div>
</div>
)}
<Suspense
fallback={<div className="flex justify-center p-8">Carregando...</div>}
>
{custaDialogOpen && (
<DialogCustaProcessual
key="custa-dialog" // Adicionar key única
open={custaDialogOpen}
onClose={() => {
setCustaDialogOpen(false);
setEditingCusta(null);
}}
onSave={handleSaveCusta}
editingCusta={editingCusta}
/>
)}
{honorarioDialogOpen && (
<DialogHonorario
key="honorario-dialog" // Adicionar key única
open={honorarioDialogOpen}
onClose={() => {
setHonorarioDialogOpen(false);
setEditingHonorario(null);
}}
onSave={handleSaveHonorario}
editingHonorario={editingHonorario}
/>
)}
{parcelaDialogOpen && (
<DialogParcela
key="parcela-dialog" // Adicionar key única
isOpen={parcelaDialogOpen}
onClose={() => {
setParcelaDialogOpen(false);
setEditingParcela(null);
}}
onSave={handleUpdateParcela}
parcela={editingParcela}
tipo={parcelaTipo}
/>
)}
</Suspense>
</div>
);
}
+151
View File
@@ -0,0 +1,151 @@
import { useProcessoContext } from "../../context/ProcessoContext";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
export function AbaInformacoes() {
const { processo } = useProcessoContext();
if (!processo) {
return <div>Carregando...</div>;
}
// Encontrar próxima audiência
const proximaAudiencia = processo.dadosDatajud?.movimentos?.find((mov) => {
const isAudiencia =
mov.nome.toLowerCase().includes("audiência") &&
mov.complementosTabelados?.some((c) => c.nome === "designada");
if (!isAudiencia) return false;
const dataAudiencia = new Date(mov.dataHora);
return dataAudiencia > new Date();
});
return (
<div className="space-y-6">
<div className="bg-white rounded-xl p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">
Informações do Processo
</h3>
{/* Informações Básicas */}
<div className="grid grid-cols-2 gap-4">
<InfoItem label="Número do Processo" value={processo.numero} />
<InfoItem label="Natureza" value={processo.natureza} />
<InfoItem label="Classe" value={processo.classeProcessual} />
<InfoItem label="Órgão Julgador" value={processo.orgaoJulgador} />
<InfoItem
label="Tribunal"
value={`${processo.tribunal} (${processo.tribunalUF})`}
/>
<InfoItem
label="Valor da Causa"
value={
processo.valorCausa
? new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(processo.valorCausa)
: "Não informado"
}
/>
</div>
{/* Dados do DataJud */}
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<h3 className="text-md font-medium text-gray-900 mb-4">
Dados do DataJud
</h3>
<div className="grid grid-cols-2 gap-4">
<InfoItem
label="Classe"
value={processo.dadosDatajud?.classe?.nome}
/>
<InfoItem
label="Data de Ajuizamento"
value={
processo.dataAjuizamento
? format(new Date(processo.dataAjuizamento), "dd/MM/yyyy", {
locale: ptBR,
})
: undefined
}
/>
<InfoItem label="Grau" value={processo.dadosDatajud?.grau} />
<InfoItem
label="Formato"
value={processo.dadosDatajud?.formato?.nome}
/>
<InfoItem
label="Sistema"
value={processo.dadosDatajud?.sistema?.nome}
/>
<InfoItem
label="Órgão Julgador"
value={processo.dadosDatajud?.orgaoJulgador?.nome}
/>
</div>
{/* Assuntos do DataJud */}
{processo.dadosDatajud?.assuntos &&
processo.dadosDatajud.assuntos.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium text-gray-900 mb-2">
Assuntos
</h4>
<div className="flex flex-wrap gap-2">
{processo.dadosDatajud.assuntos.map((assunto) => (
<span
key={assunto.codigo}
className="inline-flex items-center px-3 py-1 rounded-full text-sm
bg-blue-50 text-blue-700 border border-blue-200"
>
{assunto.nome}
</span>
))}
</div>
</div>
)}
</div>
{/* Próxima Audiência */}
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">Próxima Audiência</h4>
{proximaAudiencia ? (
<div className="space-y-2">
<p className="text-sm text-gray-900">
<span className="font-medium">Tipo:</span>{" "}
{proximaAudiencia.nome}
</p>
<p className="text-sm text-gray-900">
<span className="font-medium">Data:</span>{" "}
{format(
new Date(proximaAudiencia.dataHora),
"dd/MM/yyyy 'às' HH:mm",
{ locale: ptBR },
)}
</p>
</div>
) : (
<p className="text-sm text-gray-500">Nenhuma audiência agendada</p>
)}
</div>
</div>
</div>
);
}
function InfoItem({
label,
value,
}: {
label: string;
value: string | null | undefined;
}) {
return (
<div>
<dt className="text-sm font-medium text-gray-500">{label}</dt>
<dd className="mt-1 text-sm text-gray-900">{value || "Não informado"}</dd>
</div>
);
}
@@ -0,0 +1,88 @@
import { useProcessoContext } from "../../context/ProcessoContext";
import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { ClockIcon, DocumentIcon } from "@heroicons/react/24/outline";
export default function AbaMovimentacoes() {
const { processo } = useProcessoContext();
const { movimentacoes, loading, error } = useProcessoMovimentacoes(processo);
if (loading) {
return (
<div className="flex justify-center p-8">Carregando movimentações...</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center p-8 text-red-600">
<span>Erro ao carregar movimentações</span>
<span className="text-sm">{error}</span>
</div>
);
}
return (
<div className="bg-white rounded-xl p-6">
<div className="flow-root">
<ul role="list" className="-mb-8">
{movimentacoes.map((movimentacao, idx) => {
// Criar um identificador único usando todas as informações disponíveis
const movId = `${movimentacao.codigo}-${movimentacao.nome.slice(0, 10)}-${movimentacao.dataHora}-${idx}`;
return (
<li key={movId}>
<div className="relative pb-8">
{idx !== movimentacoes.length - 1 && (
<span
className="absolute left-4 top-4 -ml-px h-full w-0.5 bg-gray-200"
aria-hidden="true"
/>
)}
<div className="relative flex space-x-3">
<div>
<span className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-50">
{movimentacao.complementosTabelados?.length > 0 ? (
<DocumentIcon className="h-5 w-5 text-blue-600" />
) : (
<ClockIcon className="h-5 w-5 text-blue-600" />
)}
</span>
</div>
<div className="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p className="text-sm font-medium text-gray-900">
{movimentacao.nome}
</p>
{movimentacao.complementosTabelados?.map(
(complemento) => (
<p
key={complemento.codigo}
className="mt-0.5 text-sm text-gray-500"
>
{complemento.nome}
</p>
),
)}
</div>
<div className="whitespace-nowrap text-right text-sm text-gray-500">
{format(
new Date(movimentacao.dataHora),
"dd/MM/yyyy 'às' HH:mm",
{
locale: ptBR,
},
)}
</div>
</div>
</div>
</div>
</li>
);
})}
</ul>
</div>
</div>
);
}
+147
View File
@@ -0,0 +1,147 @@
import React from "react";
import { Dialog } from "@headlessui/react";
import { useForm } from "react-hook-form";
import { FormSelect, FormInput } from "../forms";
const TIPOS_AUDIENCIA = [
{ value: "INSTRUCAO", label: "Instrução" },
{ value: "INSTRUCAO_JULGAMENTO", label: "Instrução e Julgamento" },
{ value: "CONCILIACAO", label: "Conciliação" },
{ value: "PERICIA", label: "Perícia" },
{ value: "ESTUDO_PSICOPEDAGOGICO", label: "Estudo Psicopedagógico" },
{ value: "MEDIACAO", label: "Mediação" },
];
interface AudienciaModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: any) => void;
editingAudiencia?: any;
isFinalizacao?: boolean; // Nova prop
}
export const AudienciaModal: React.FC<AudienciaModalProps> = ({
isOpen,
onClose,
onSave,
editingAudiencia,
isFinalizacao = false,
}) => {
const { register, handleSubmit } = useForm({
defaultValues: editingAudiencia || {},
});
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-md bg-white rounded-lg p-6">
<Dialog.Title className="text-lg font-medium mb-4">
{isFinalizacao
? "Finalizar Audiência"
: editingAudiencia
? "Editar Audiência"
: "Nova Audiência"}
</Dialog.Title>
<form onSubmit={handleSubmit(onSave)} className="space-y-4">
{!isFinalizacao ? (
<>
<FormSelect
label="Tipo de Audiência"
{...register("tipo")}
options={TIPOS_AUDIENCIA}
required
/>
<FormInput
label="Data e Hora"
type="datetime-local"
{...register("data")}
required
/>
<FormInput label="Local" {...register("local")} required />
<FormSelect
label="Modalidade"
{...register("modalidade")}
options={[
{ value: "PRESENCIAL", label: "Presencial" },
{ value: "VIRTUAL", label: "Virtual" },
]}
/>
<FormInput
label="Link (para audiência virtual)"
{...register("linkVirtual")}
/>
<FormInput
label="Observações"
{...register("observacoes")}
as="textarea"
/>
</>
) : (
<>
<div className="bg-gray-50 p-4 rounded-lg mb-4">
<h3 className="font-medium text-gray-900">
Informações da Audiência
</h3>
<p className="text-sm text-gray-600 mt-1">
Tipo: {editingAudiencia?.tipo}
<br />
Data: {new Date(editingAudiencia?.data).toLocaleString()}
<br />
Local: {editingAudiencia?.local}
</p>
</div>
<FormInput
label="Relatório da Audiência"
{...register("relatorio")}
as="textarea"
rows={6}
placeholder="Descreva o que ocorreu na audiência..."
required
/>
<FormSelect
label="Status Final"
{...register("status")}
options={[
{ value: "REALIZADA", label: "Realizada" },
{ value: "CANCELADA", label: "Cancelada" },
{ value: "REDESIGNADA", label: "Redesignada" },
]}
required
/>
</>
)}
<div className="flex justify-end space-x-2 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-50 rounded-lg"
>
Cancelar
</button>
<button
type="submit"
className={`px-4 py-2 text-sm text-white rounded-lg
${
isFinalizacao
? "bg-green-600 hover:bg-green-700"
: "bg-blue-600 hover:bg-blue-700"
}`}
>
{isFinalizacao ? "Finalizar" : "Salvar"}
</button>
</div>
</form>
</Dialog.Panel>
</div>
</Dialog>
);
};
@@ -0,0 +1,262 @@
import {
Dialog,
TextField,
MenuItem,
Button,
Box,
Typography,
} from "@mui/material";
import { useState, useEffect } from "react";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
interface DialogCustaProcessualProps {
open: boolean;
onClose: () => void;
onSave: (data: any) => Promise<void>;
editingCusta?: any;
}
export function DialogCustaProcessual({
open,
onClose,
onSave,
editingCusta,
}: DialogCustaProcessualProps) {
const [formData, setFormData] = useState({
tipo: "",
descricao: "",
valor: "",
percentual: "",
status: "PENDENTE",
vencimento: null as Date | null,
dataPagamento: null as Date | null,
observacoes: "",
numeroParcelas: 1,
parcelaAtual: 1,
comprovante: "",
});
const [formError, setFormError] = useState<string | null>(null);
// Atualizar form quando o editingCusta muda
useEffect(() => {
if (editingCusta) {
setFormData({
tipo: editingCusta.tipo || "",
descricao: editingCusta.descricao || "",
valor: editingCusta.valor?.toString() || "",
percentual: editingCusta.percentual?.toString() || "",
status: editingCusta.status || "PENDENTE",
vencimento: editingCusta.vencimento
? new Date(editingCusta.vencimento)
: null,
dataPagamento: editingCusta.dataPagamento
? new Date(editingCusta.dataPagamento)
: null,
observacoes: editingCusta.observacoes || "",
numeroParcelas: editingCusta.numeroParcelas || 1,
parcelaAtual: editingCusta.parcelaAtual || 1,
comprovante: editingCusta.comprovante || "",
});
} else {
// Reset para os valores padrão
setFormData({
tipo: "",
descricao: "",
valor: "",
percentual: "",
status: "PENDENTE",
vencimento: null,
dataPagamento: null,
observacoes: "",
numeroParcelas: 1,
parcelaAtual: 1,
comprovante: "",
});
}
}, [editingCusta, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
// Validar campos obrigatórios
if (!formData.tipo) {
setFormError("Selecione o tipo da custa");
return;
}
if (!formData.descricao) {
setFormError("Informe a descrição da custa");
return;
}
if (!formData.valor) {
setFormError("Informe o valor da custa");
return;
}
// Preparar dados
const preparedData = {
...formData,
valor: Number(formData.valor),
percentual: formData.percentual ? Number(formData.percentual) : null,
numeroParcelas: Number(formData.numeroParcelas),
parcelaAtual: Number(formData.parcelaAtual),
};
await onSave(preparedData);
onClose();
} catch (error: any) {
console.error("Erro ao salvar custa:", error);
setFormError(error.message || "Erro ao salvar custa. Tente novamente.");
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<Box component="form" onSubmit={handleSubmit} sx={{ p: 3 }}>
<h2 className="text-lg font-semibold mb-4">
{editingCusta ? "Editar Custa Processual" : "Nova Custa Processual"}
</h2>
<div className="space-y-4">
<TextField
select
fullWidth
label="Tipo"
value={formData.tipo}
onChange={(e) => setFormData({ ...formData, tipo: e.target.value })}
required
>
{Object.values(TipoCustaProcessual).map((tipo) => (
<MenuItem key={tipo} value={tipo}>
{tipo.replace(/_/g, " ")}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="Descrição"
value={formData.descricao}
onChange={(e) =>
setFormData({ ...formData, descricao: e.target.value })
}
required
/>
<TextField
fullWidth
label="Valor"
type="number"
value={formData.valor}
onChange={(e) =>
setFormData({ ...formData, valor: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
<TextField
fullWidth
label="Percentual (se aplicável)"
type="number"
value={formData.percentual}
onChange={(e) =>
setFormData({ ...formData, percentual: e.target.value })
}
inputProps={{ step: "0.01", min: "0", max: "100" }}
/>
<LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={ptBR}
>
<DatePicker
label="Vencimento"
value={formData.vencimento}
onChange={(date) =>
setFormData({ ...formData, vencimento: date })
}
sx={{ width: "100%", mb: 2 }}
/>
{formData.status === "PAGO" && (
<DatePicker
label="Data de Pagamento"
value={formData.dataPagamento}
onChange={(date) =>
setFormData({ ...formData, dataPagamento: date })
}
sx={{ width: "100%" }}
/>
)}
</LocalizationProvider>
<TextField
select
fullWidth
label="Status"
value={formData.status}
onChange={(e) =>
setFormData({ ...formData, status: e.target.value })
}
required
>
{Object.values(StatusPagamento).map((status) => (
<MenuItem key={status} value={status}>
{status.replace(/_/g, " ")}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="Número de Parcelas"
type="number"
value={formData.numeroParcelas}
onChange={(e) =>
setFormData({
...formData,
numeroParcelas: parseInt(e.target.value),
})
}
inputProps={{ min: 1 }}
/>
<TextField
fullWidth
label="Observações"
multiline
rows={3}
value={formData.observacoes}
onChange={(e) =>
setFormData({ ...formData, observacoes: e.target.value })
}
/>
</div>
{formError && (
<Box sx={{ mt: 2, p: 2, bgcolor: "error.light", borderRadius: 1 }}>
<Typography color="error">{formError}</Typography>
</Box>
)}
<Box
sx={{ mt: 3, display: "flex", justifyContent: "flex-end", gap: 2 }}
>
<Button onClick={onClose}>Cancelar</Button>
<Button type="submit" variant="contained" color="primary">
Salvar
</Button>
</Box>
</Box>
</Dialog>
);
}
export default DialogCustaProcessual;
@@ -0,0 +1,347 @@
import {
Dialog,
TextField,
MenuItem,
Button,
Box,
FormControl,
InputLabel,
Select,
Typography,
} from "@mui/material";
import { useState, useEffect } from "react";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
import { IHonorario, IHonorarioCreate } from "../types/financeiro";
interface DialogHonorarioProps {
open: boolean;
onClose: () => void;
onSave: (data: Partial<IHonorarioCreate>) => Promise<void>;
editingHonorario?: IHonorario;
}
export function DialogHonorario({
open,
onClose,
onSave,
editingHonorario,
}: DialogHonorarioProps) {
const [formData, setFormData] = useState({
formaPagamento: "VISTA",
valorFixo: "",
percentualCausa: "",
percentualExito: "",
valorEntrada: "",
numeroParcelas: 1,
valorParcela: "",
diaVencimento: 10,
status: "PENDENTE",
dataVencimento: new Date(),
observacoes: "",
});
const [formError, setFormError] = useState<string | null>(null);
// Atualizar form quando o editingHonorario muda
useEffect(() => {
if (editingHonorario) {
setFormData({
formaPagamento: editingHonorario.formaPagamento,
valorFixo: editingHonorario.valorFixo || "",
percentualCausa: editingHonorario.percentualCausa || "",
percentualExito: editingHonorario.percentualExito || "",
valorEntrada: editingHonorario.valorEntrada || "",
numeroParcelas: editingHonorario.numeroParcelas || 1,
valorParcela: editingHonorario.valorParcela || "",
diaVencimento: editingHonorario.diaVencimento || 10,
status: editingHonorario.status,
dataVencimento: editingHonorario.dataVencimento
? new Date(editingHonorario.dataVencimento)
: new Date(),
observacoes: editingHonorario.observacoes || "",
});
} else {
// Reset para os valores padrão
setFormData({
formaPagamento: "VISTA",
valorFixo: "",
percentualCausa: "",
percentualExito: "",
valorEntrada: "",
numeroParcelas: 1,
valorParcela: "",
diaVencimento: 10,
status: "PENDENTE",
dataVencimento: new Date(),
observacoes: "",
});
}
}, [editingHonorario, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
// Validações básicas
const validationError = validateFormData(formData);
if (validationError) {
setFormError(validationError);
return;
}
// Converter strings numéricas
const preparedData = {
...formData,
valorFixo: formData.valorFixo ? Number(formData.valorFixo) : null,
percentualCausa: formData.percentualCausa
? Number(formData.percentualCausa)
: null,
percentualExito: formData.percentualExito
? Number(formData.percentualExito)
: null,
valorEntrada: formData.valorEntrada
? Number(formData.valorEntrada)
: null,
valorParcela: formData.valorParcela
? Number(formData.valorParcela)
: null,
};
await onSave(preparedData);
onClose();
} catch (error: any) {
console.error("Erro ao salvar honorário:", error);
setFormError(
error.message || "Erro ao salvar honorário. Tente novamente.",
);
}
};
const validateFormData = (data: any): string | null => {
if (!data.formaPagamento) {
return "Forma de pagamento é obrigatória";
}
switch (data.formaPagamento) {
case "VISTA":
if (!data.valorFixo) {
return "Informe o valor do honorário";
}
break;
case "PERCENTUAL_CAUSA":
if (!data.percentualCausa) {
return "Informe o percentual da causa";
}
break;
case "PERCENTUAL_EXITO":
if (!data.percentualExito) {
return "Informe o percentual do êxito";
}
break;
case "PARCELADO":
case "ENTRADA_MAIS_PARCELAS":
if (!data.valorParcela) {
return "Informe o valor da parcela";
}
if (!data.numeroParcelas || data.numeroParcelas < 1) {
return "Informe o número de parcelas";
}
break;
}
return null;
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<Box component="form" onSubmit={handleSubmit} sx={{ p: 3 }}>
<h2 className="text-lg font-semibold mb-4">
{editingHonorario ? "Editar Honorário" : "Novo Honorário"}
</h2>
<div className="space-y-4">
<FormControl fullWidth>
<InputLabel>Forma de Pagamento</InputLabel>
<Select
value={formData.formaPagamento}
label="Forma de Pagamento"
onChange={(e) =>
setFormData({ ...formData, formaPagamento: e.target.value })
}
>
{Object.values(FormaPagamento).map((forma) => (
<MenuItem key={forma} value={forma}>
{forma.replace(/_/g, " ")}
</MenuItem>
))}
</Select>
</FormControl>
{formData.formaPagamento === "VISTA" && (
<TextField
fullWidth
label="Valor"
type="number"
value={formData.valorFixo}
onChange={(e) =>
setFormData({ ...formData, valorFixo: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
)}
{formData.formaPagamento === "PERCENTUAL_CAUSA" && (
<TextField
fullWidth
label="Percentual da Causa"
type="number"
value={formData.percentualCausa}
onChange={(e) =>
setFormData({ ...formData, percentualCausa: e.target.value })
}
required
inputProps={{ step: "0.01", min: "0", max: "100" }}
/>
)}
{formData.formaPagamento === "PERCENTUAL_EXITO" && (
<TextField
fullWidth
label="Percentual do Êxito"
type="number"
value={formData.percentualExito}
onChange={(e) =>
setFormData({ ...formData, percentualExito: e.target.value })
}
required
inputProps={{ step: "0.01", min: "0", max: "100" }}
/>
)}
{(formData.formaPagamento === "PARCELADO" ||
formData.formaPagamento === "ENTRADA_MAIS_PARCELAS") && (
<>
{formData.formaPagamento === "ENTRADA_MAIS_PARCELAS" && (
<TextField
fullWidth
label="Valor da Entrada"
type="number"
value={formData.valorEntrada}
onChange={(e) =>
setFormData({ ...formData, valorEntrada: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
)}
<TextField
fullWidth
label="Número de Parcelas"
type="number"
value={formData.numeroParcelas}
onChange={(e) =>
setFormData({
...formData,
numeroParcelas: parseInt(e.target.value),
})
}
required
inputProps={{ min: "1" }}
/>
<TextField
fullWidth
label="Valor da Parcela"
type="number"
value={formData.valorParcela}
onChange={(e) =>
setFormData({ ...formData, valorParcela: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
<TextField
fullWidth
label="Dia do Vencimento"
type="number"
value={formData.diaVencimento}
onChange={(e) =>
setFormData({
...formData,
diaVencimento: parseInt(e.target.value),
})
}
inputProps={{ min: 1, max: 31 }}
required
/>
</>
)}
<TextField
select
fullWidth
label="Status"
value={formData.status}
onChange={(e) =>
setFormData({ ...formData, status: e.target.value })
}
required
>
{Object.values(StatusPagamento).map((status) => (
<MenuItem key={status} value={status}>
{status.replace(/_/g, " ")}
</MenuItem>
))}
</TextField>
<LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={ptBR}
>
<DatePicker
label="Data de Vencimento"
value={formData.dataVencimento}
onChange={(date) =>
setFormData({ ...formData, dataVencimento: date || new Date() })
}
sx={{ width: "100%" }}
/>
</LocalizationProvider>
<TextField
fullWidth
label="Observações"
multiline
rows={3}
value={formData.observacoes}
onChange={(e) =>
setFormData({ ...formData, observacoes: e.target.value })
}
/>
</div>
{formError && (
<Box sx={{ mt: 2, p: 2, bgcolor: "error.light", borderRadius: 1 }}>
<Typography color="error">{formError}</Typography>
</Box>
)}
<Box
sx={{ mt: 3, display: "flex", justifyContent: "flex-end", gap: 2 }}
>
<Button onClick={onClose}>Cancelar</Button>
<Button type="submit" variant="contained" color="primary">
Salvar
</Button>
</Box>
</Box>
</Dialog>
);
}
export default DialogHonorario;
@@ -0,0 +1,72 @@
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useProcessoMovimentacoes } from "../../hooks/useProcessoMovimentacoes";
interface MovimentacoesTimelineProps {
numeroProcesso: string;
tribunal: string;
}
export function MovimentacoesTimeline({
numeroProcesso,
tribunal,
}: MovimentacoesTimelineProps) {
const { movimentacoes, loading, error } = useProcessoMovimentacoes(
numeroProcesso,
tribunal,
);
if (loading) {
return <div className="text-center p-4">Carregando movimentações...</div>;
}
if (error) {
return (
<div className="text-center p-4 text-red-600">
Erro ao carregar movimentações
</div>
);
}
if (!movimentacoes?.length) {
return (
<div className="text-center p-4">Nenhuma movimentação encontrada</div>
);
}
return (
<div className="flow-root">
<ul role="list" className="-mb-8">
{movimentacoes.map((mov, idx) => (
<li key={`${mov.codigo}-${mov.dataHora}`}>
<div className="relative pb-8">
{idx !== movimentacoes.length - 1 ? (
<span className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200" />
) : null}
<div className="relative flex space-x-3">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100">
<span className="text-xs text-gray-500">{mov.codigo}</span>
</div>
<div className="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p className="text-sm text-gray-900">{mov.nome}</p>
{mov.complementosTabelados?.map((comp: any) => (
<p key={comp.codigo} className="text-sm text-gray-500">
{comp.nome}
</p>
))}
</div>
<div className="whitespace-nowrap text-right text-sm text-gray-500">
{format(new Date(mov.dataHora), "dd/MM/yyyy 'às' HH:mm", {
locale: ptBR,
})}
</div>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,149 @@
import React, { useState } from "react";
import { Dialog } from "@headlessui/react";
import { useClientes } from "../../hooks/useClientes";
import { FormSelect, FormInput } from "../forms";
import { useForm } from "react-hook-form";
interface ParteProcessoModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: any) => void;
tipo: "AUTOR" | "REU";
editingParte?: any;
}
export const ParteProcessoModal: React.FC<ParteProcessoModalProps> = ({
isOpen,
onClose,
onSave,
tipo,
editingParte,
}) => {
const { register, handleSubmit, setValue, watch } = useForm({
defaultValues: editingParte || {
tipo, // Já estava aqui, mas precisamos garantir que está sendo enviado
principal: false,
},
});
const { clientes } = useClientes();
const [useExistingClient, setUseExistingClient] = useState(
!!editingParte?.clienteId,
);
const [searchTerm, setSearchTerm] = useState("");
const filteredClientes = clientes.filter(
(cliente) =>
cliente.nome.toLowerCase().includes(searchTerm.toLowerCase()) ||
cliente.cpfCnpj.includes(searchTerm),
);
const onSubmit = (data: any) => {
const parteData = {
...data,
tipo, // Garantir que o tipo está sendo enviado
nome: useExistingClient
? clientes.find((c) => c.id === data.clienteId)?.nome
: data.nome,
cpfCnpj: useExistingClient
? clientes.find((c) => c.id === data.clienteId)?.cpfCnpj
: data.cpfCnpj,
clienteId: useExistingClient ? data.clienteId : undefined,
principal: data.principal || false,
};
console.log("Enviando dados:", parteData); // Debug
onSave(parteData);
onClose();
};
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
{/* ... dialog backdrop ... */}
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-md bg-white rounded-lg p-6">
<Dialog.Title className="text-lg font-medium mb-4">
Adicionar {tipo === "AUTOR" ? "Autor" : "Réu"}
</Dialog.Title>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
{/* Adicionar campo oculto para o tipo */}
<input type="hidden" {...register("tipo")} value={tipo} />
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={useExistingClient}
onChange={(e) => setUseExistingClient(e.target.checked)}
className="rounded border-gray-300"
/>
<label className="text-sm">Usar cliente existente</label>
</div>
{useExistingClient ? (
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
Buscar Cliente
</label>
<input
type="text"
className="w-full px-3 py-2 border border-gray-300 rounded-md"
placeholder="Digite nome ou CPF/CNPJ..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<div className="mt-2 max-h-60 overflow-y-auto border border-gray-200 rounded-lg">
{filteredClientes.map((cliente) => (
<label
key={cliente.id}
className="flex items-center p-2 hover:bg-gray-50 cursor-pointer"
>
<input
type="radio"
{...register("clienteId")}
value={cliente.id}
className="mr-2"
/>
<div>
<div className="font-medium">{cliente.nome}</div>
<div className="text-sm text-gray-500">
{cliente.cpfCnpj}
</div>
</div>
</label>
))}
</div>
</div>
) : (
<>
<FormInput label="Nome" {...register("nome")} required />
<FormInput label="CPF/CNPJ" {...register("cpfCnpj")} />
</>
)}
<FormInput
label="Observações"
{...register("observacoes")}
as="textarea"
/>
<div className="flex justify-end space-x-2 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-50 rounded-lg"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg"
>
Salvar
</button>
</div>
</form>
</Dialog.Panel>
</div>
</Dialog>
);
};
Whitespace-only changes.
+58
View File
@@ -0,0 +1,58 @@
import React from 'react';
import { Processo } from '../../types/entities';
interface ProcessoCardProps {
processo: Processo;
}
const resultadoLabel = {
NAO_JULGADO: 'Não Julgado',
PROCEDENTE: 'Procedente',
PARCIALMENTE_PROCEDENTE: 'Parcialmente Procedente',
IMPROCEDENTE: 'Improcedente',
SEM_ANALISE_MERITO: 'Sem Análise de Mérito'
};
const resultadoColor = {
NAO_JULGADO: 'bg-gray-100 text-gray-800',
PROCEDENTE: 'bg-green-100 text-green-800',
PARCIALMENTE_PROCEDENTE: 'bg-yellow-100 text-yellow-800',
IMPROCEDENTE: 'bg-red-100 text-red-800',
SEM_ANALISE_MERITO: 'bg-gray-100 text-gray-800'
};
export const ProcessoCard: React.FC<ProcessoCardProps> = ({ processo }) => {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
{processo.numero}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">
{processo.cliente.nome}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{processo.nomeContraparte}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{processo.natureza}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{processo.status}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{processo.valorCausa?.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(processo.dataCriacao).toLocaleDateString('pt-BR')}
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(processo.dataAtualizacao).toLocaleDateString('pt-BR')}
</p>
<div className="mt-2">
<span className={`px-2 py-1 text-xs rounded-full ${resultadoColor[processo.resultado]}`}>
{resultadoLabel[processo.resultado]}
</span>
</div>
</div>
);
};
@@ -0,0 +1,646 @@
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>
);
};
+152
View File
@@ -0,0 +1,152 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { Processo } from '../../types/entities';
interface ProcessoFormProps {
onSubmit: (data: Partial<Processo>) => void;
initialData?: Partial<Processo>;
}
const resultadoOptions = [
{ value: 'NAO_JULGADO', label: 'Não Julgado' },
{ value: 'PROCEDENTE', label: 'Procedente' },
{ value: 'PARCIALMENTE_PROCEDENTE', label: 'Parcialmente Procedente' },
{ value: 'IMPROCEDENTE', label: 'Improcedente' },
{ value: 'SEM_ANALISE_MERITO', label: 'Sem Análise de Mérito' }
];
export const ProcessoForm: React.FC<ProcessoFormProps> = ({ onSubmit, initialData }) => {
const { register, handleSubmit, reset } = useForm<Partial<Processo>>({
defaultValues: initialData,
});
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Número do Processo
</label>
<input
{...register('numero')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Natureza
</label>
<select
{...register('natureza')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
>
<option value="CIVEL">Cível</option>
<option value="CRIMINAL">Criminal</option>
<option value="PREVIDENCIARIO">Previdenciário</option>
<option value="TRABALHISTA">Trabalhista</option>
<option value="FAMILIA">Família</option>
<option value="TRIBUTARIO">Tributário</option>
<option value="ADMINISTRATIVO">Administrativo</option>
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
{...register('status')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
>
<option value="INICIAL">Inicial</option>
<option value="EM_ANDAMENTO">Em Andamento</option>
<option value="SUSPENSO">Suspenso</option>
<option value="RECURSO">Recurso</option>
<option value="CONCLUSO">Concluso</option>
<option value="ARQUIVADO">Arquivado</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Resultado do Julgamento
</label>
<select
{...register('resultado')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
>
{resultadoOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Cliente
</label>
<input
{...register('clienteId')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Valor da Causa
</label>
<input
type="number"
{...register('valorCausa')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Órgão Julgador
</label>
<input
{...register('orgaoJulgador')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nome da Contraparte
</label>
<input
{...register('nomeContraparte')}
className="w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700"
/>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
type="button"
onClick={() => reset()}
className="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg"
>
Salvar
</button>
</div>
</form>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { DatajudProcesso } from "../../types/datajud";
interface ProcessoInfoProps {
data: DatajudProcesso;
}
export const ProcessoInfo = ({ data }: ProcessoInfoProps) => {
return (
<div className="bg-white rounded-lg shadow p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<InfoItem label="Classe" value={data.classe.nome} />
<InfoItem
label="Data de Ajuizamento"
value={format(new Date(data.dataAjuizamento), "dd/MM/yyyy", {
locale: ptBR,
})}
/>
<InfoItem label="Grau" value={data.grau} />
<InfoItem label="Formato" value={data.formato.nome} />
<InfoItem label="Sistema" value={data.sistema.nome} />
<InfoItem label="Órgão Julgador" value={data.orgaoJulgador.nome} />
</div>
{data.assuntos.length > 0 && (
<div className="mt-4">
<h4 className="font-medium text-gray-700 mb-2">Assuntos</h4>
<div className="flex flex-wrap gap-2">
{data.assuntos.map((assunto) => (
<span
key={assunto.codigo}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
{assunto.nome}
</span>
))}
</div>
</div>
)}
</div>
);
};
function InfoItem({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-sm font-medium text-gray-500">{label}</dt>
<dd className="mt-1 text-sm text-gray-900">{value}</dd>
</div>
);
}
export default ProcessoInfo;
+443
View File
@@ -0,0 +1,443 @@
import React, { useState, useEffect } from "react";
import { Dialog, Transition, Combobox } from "@headlessui/react";
import {
XMarkIcon,
MagnifyingGlassIcon,
CheckIcon,
} from "@heroicons/react/24/outline";
import { useForm } from "react-hook-form";
import { Processo, NaturezaProcesso, TipoParte } from "../../types/entities";
import { useProcessos } from "../../hooks/useProcessos";
import { useClientes } from "../../hooks/useClientes";
import { FormInput, FormSelect, FormCheckbox } from "../forms";
import axios from "axios";
import { UF_BRASIL, TRIBUNAIS } from "../../utils/constants/tribunais";
import { useDatajud } from "../../hooks/useDatajud";
import { ProcessoInfo } from "./ProcessoInfo"; // Adicionar import
import { DatajudProcesso } from "../../types/datajud";
interface ProcessoModalProps {
isOpen: boolean;
onClose: () => void;
processo: Processo | null;
onSuccess?: () => void;
}
interface OrgaoJulgadorOption {
id: string;
nome: string;
}
interface OrgaoSugestao {
id: string;
nome: string;
}
export const ProcessoModal: React.FC<ProcessoModalProps> = ({
isOpen,
onClose,
processo,
onSuccess,
}) => {
const [copiarOrgaoJulgador, setCopiarOrgaoJulgador] = useState(false);
const { register, handleSubmit, setValue, watch, reset } = useForm();
const { createProcesso, updateProcesso } = useProcessos();
const { clientes } = useClientes(); // Adicionar hook de clientes
const [clienteSearch, setClienteSearch] = useState("");
const [selectedOrgao, setSelectedOrgao] =
useState<OrgaoJulgadorOption | null>(null);
const [orgaos, setOrgaos] = useState<string[]>([]);
const [orgaoQuery, setOrgaoQuery] = useState("");
const [selectedUF, setSelectedUF] = useState(processo?.tribunalUF || "");
const [availableTribunais, setAvailableTribunais] = useState<any[]>([]);
const { consultarProcesso } = useDatajud();
const [dadosDatajud, setDadosDatajud] = useState<DatajudProcesso | null>(
null,
);
const orgaoJulgador = watch("orgaoJulgador");
useEffect(() => {
if (copiarOrgaoJulgador) {
setValue("orgaoOrigem", orgaoJulgador);
}
}, [orgaoJulgador, copiarOrgaoJulgador, setValue]);
// Adicionar useEffect para popular form quando editando
useEffect(() => {
if (isOpen && processo) {
console.log("Populando formulário com:", processo);
Object.keys(processo).forEach((key) => {
if (
key !== "id" &&
key !== "dataCriacao" &&
key !== "dataAtualizacao"
) {
setValue(key, processo[key]);
}
});
if (processo.orgaoJulgadorRef) {
setSelectedOrgao(processo.orgaoJulgadorRef);
setValue("orgaoJulgador", processo.orgaoJulgadorRef.nome);
}
// Definir UF e tribunal ao editar
setSelectedUF(processo.tribunalUF || "");
setValue("tribunal", processo.tribunal || "");
} else {
reset({
numero: "",
natureza: "",
tipoParteCliente: "",
valorCausa: "",
orgaoJulgador: "",
orgaoOrigem: "",
nomeContraparte: "",
clienteId: "",
tribunalUF: "",
tribunal: "",
});
setSelectedUF("");
}
}, [isOpen, processo, setValue, reset]);
useEffect(() => {
if (isOpen) {
// Tentar buscar órgãos existentes
const loadOrgaos = async () => {
try {
const response = await axios.get<string[]>("/api/processos/orgaos");
if (Array.isArray(response.data)) {
setOrgaos(response.data.filter(Boolean));
}
} catch (error) {
console.error("Erro ao carregar órgãos:", error);
setOrgaos([]); // Em caso de erro, inicializar com array vazio
}
};
loadOrgaos();
}
}, [isOpen]);
useEffect(() => {
if (selectedUF) {
setAvailableTribunais(TRIBUNAIS[selectedUF] || []);
} else {
setAvailableTribunais([]);
}
}, [selectedUF]);
const onSubmit = async (data: any) => {
try {
const processData = {
...data,
numero: data.numero.replace(/\D/g, ""),
tribunalUF: selectedUF,
tribunal: data.tribunal,
dadosDatajud: dadosDatajud,
valorCausa: data.valorCausa ? Number(data.valorCausa) : undefined,
};
if (processo) {
await updateProcesso.mutateAsync({
id: processo.id,
data: processData
});
} else {
await createProcesso.mutateAsync(processData);
}
onSuccess?.();
onClose();
} catch (error) {
console.error("Erro ao salvar processo:", error);
}
};
// Filtrar clientes baseado na busca
const filteredClientes = clientes.filter(
(cliente) =>
cliente.nome.toLowerCase().includes(clienteSearch.toLowerCase()) ||
cliente.cpfCnpj.includes(clienteSearch),
);
// Filtrar órgãos baseado na busca
const filteredOrgaos =
orgaoQuery === ""
? orgaos
: orgaos.filter((orgao) =>
orgao.toLowerCase().includes(orgaoQuery.toLowerCase()),
);
const handleOrgaoSelect = (orgao: OrgaoJulgadorOption | null) => {
setSelectedOrgao(orgao);
if (orgao) {
setValue("orgaoJulgador", orgao.nome);
setValue("orgaoJulgadorId", orgao.id);
}
};
// Função para buscar dados do DataJud
const buscarDadosDatajud = async (numero: string, tribunal: string) => {
try {
const dados = await consultarProcesso(numero, tribunal);
if (dados) {
setDadosDatajud(dados);
// Preencher campos do formulário
setValue("orgaoJulgador", dados.orgaoJulgador.nome);
setValue("natureza", inferirNatureza(dados.classe.nome));
// Outros campos conforme necessário
}
} catch (error) {
console.error("Erro ao buscar dados do DataJud:", error);
}
};
// Função auxiliar para inferir natureza do processo
const inferirNatureza = (classeNome: string): string => {
const nome = classeNome.toLowerCase();
if (nome.includes("cível")) return "CIVEL";
if (nome.includes("criminal")) return "CRIMINAL";
if (nome.includes("trabalh")) return "TRABALHISTA";
// ... outras regras
return "CIVEL";
};
// Adicionar evento no campo de número do processo
useEffect(() => {
const numero = watch("numero");
const tribunal = watch("tribunal");
if (numero?.length >= 20 && tribunal) {
// Número CNJ completo
buscarDadosDatajud(numero, tribunal);
}
}, [watch("numero"), watch("tribunal")]);
return (
<Transition show={isOpen} as={React.Fragment}>
<Dialog onClose={onClose} className="relative z-50">
{/* ...existing modal backdrop code... */}
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Dialog.Panel className="mx-auto max-w-4xl w-full bg-white rounded-xl shadow-2xl">
<div className="p-6">
<div className="flex justify-between items-center mb-6">
<Dialog.Title className="text-2xl font-bold text-gray-900">
{processo ? "Editar Processo" : "Novo Processo"}
</Dialog.Title>
<button onClick={onClose}>
<XMarkIcon className="h-6 w-6 text-gray-400" />
</button>
</div>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-2 gap-4">
{/* Substituir o FormSelect por uma busca customizada */}
<div className="col-span-2 space-y-2">
<label className="block text-sm font-semibold text-gray-700">
Cliente <span className="text-red-500">*</span>
</label>
{/* Campo de busca */}
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar cliente por nome ou CPF..."
className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
value={clienteSearch}
onChange={(e) => setClienteSearch(e.target.value)}
/>
</div>
{/* Lista de clientes filtrada */}
<div className="mt-2 max-h-40 overflow-y-auto border border-gray-200 rounded-lg">
{filteredClientes.length > 0 ? (
<div className="divide-y divide-gray-200">
{filteredClientes.map((cliente) => (
<label
key={cliente.id}
className="flex items-center p-3 hover:bg-gray-50 cursor-pointer"
>
<input
type="radio"
{...register("clienteId", {
required: "Cliente é obrigatório",
})}
value={cliente.id}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<div className="ml-3">
<p className="text-sm font-medium text-gray-900">
{cliente.nome}
</p>
<p className="text-sm text-gray-500">
CPF: {cliente.cpfCnpj}
</p>
</div>
</label>
))}
</div>
) : (
<div className="p-4 text-center text-gray-500">
Nenhum cliente encontrado
</div>
)}
</div>
</div>
<FormInput
label="Número do Processo"
{...register("numero")}
placeholder="0000000-00.0000.000.0000"
required
/>
<FormSelect
label="Natureza"
{...register("natureza")}
options={Object.values(NaturezaProcesso).map((value) => ({
value,
label: value.charAt(0) + value.slice(1).toLowerCase(),
}))}
required
/>
<FormSelect
label="Tipo de Parte"
{...register("tipoParteCliente")}
options={Object.values(TipoParte).map((value) => ({
value,
label: value.charAt(0) + value.slice(1).toLowerCase(),
}))}
required
/>
<FormInput
label="Valor da Causa"
type="number"
step="0.01"
{...register("valorCausa")}
/>
<FormSelect
label="UF do Tribunal"
value={selectedUF || ""} // Garantir que nunca seja null
onChange={(e) => {
const value = e.target.value;
setSelectedUF(value);
setValue("tribunal", ""); // Limpar tribunal ao mudar UF
}}
required
options={UF_BRASIL}
/>
<FormSelect
label="Tribunal"
value={watch("tribunal") || ""} // Garantir que nunca seja null
onChange={(e) => setValue("tribunal", e.target.value)}
required
options={availableTribunais}
disabled={!selectedUF}
/>
<div className="col-span-2">
<label className="block text-sm font-semibold text-gray-700 mb-2">
Órgão Julgador <span className="text-red-500">*</span>
</label>
<div className="relative">
<input
type="text"
className="w-full px-4 py-3 rounded-lg border border-gray-300 focus:ring-2 focus:ring-blue-500"
{...register("orgaoJulgador", { required: true })}
onChange={(e) => {
setValue("orgaoJulgador", e.target.value);
setOrgaoQuery(e.target.value);
}}
autoComplete="off"
placeholder="Digite o órgão julgador..."
/>
{/* Lista de sugestões */}
{orgaoQuery && (
<ul className="absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-60 overflow-auto">
{filteredOrgaos.map((orgao, index) => (
<li
key={index}
className="px-4 py-2 hover:bg-blue-50 cursor-pointer"
onClick={() => {
setValue("orgaoJulgador", orgao);
setOrgaoQuery("");
}}
>
{orgao}
</li>
))}
{filteredOrgaos.length === 0 && (
<li className="px-4 py-2 text-gray-500 italic">
Pressione Enter para adicionar "{orgaoQuery}"
</li>
)}
</ul>
)}
</div>
</div>
<div className="space-y-2">
<FormInput
label="Órgão de Origem"
{...register("orgaoOrigem")}
disabled={copiarOrgaoJulgador}
/>
<FormCheckbox
label="Mesmo que Órgão Julgador"
checked={copiarOrgaoJulgador}
onChange={(e) =>
setCopiarOrgaoJulgador(e.target.checked)
}
/>
</div>
<FormInput
label="Nome da Contraparte"
{...register("nomeContraparte")}
required
/>
<FormSelect
label="Status de Atividade"
{...register("statusAtividade")}
options={[
{ value: "ATIVO", label: "Ativo" },
{ value: "BAIXADO", label: "Baixado" },
{ value: "SUSPENSO", label: "Suspenso" },
]}
required
/>
</div>
{dadosDatajud && (
<div className="mt-4 p-4 bg-blue-50 rounded-lg">
<h3 className="text-sm font-medium text-blue-800 mb-2">
Dados encontrados no DataJud
</h3>
<ProcessoInfo data={dadosDatajud} />
</div>
)}
<div className="flex justify-end space-x-3 pt-6">
<button
type="button"
onClick={onClose}
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="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700"
>
{processo ? "Salvar" : "Criar"}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</div>
</div>
</Dialog>
</Transition>
);
};
+169
View File
@@ -0,0 +1,169 @@
import React from 'react';
import { useQuery } from 'react-query';
import { useParams } from 'react-router-dom';
import api from '../../services/api';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import { Processo } from '../../types/entities';
const resultadoOptions = [
{ value: 'NAO_JULGADO', label: 'Não Julgado' },
{ value: 'PROCEDENTE', label: 'Procedente' },
{ value: 'PARCIALMENTE_PROCEDENTE', label: 'Parcialmente Procedente' },
{ value: 'IMPROCEDENTE', label: 'Improcedente' },
{ value: 'SEM_ANALISE_MERITO', label: 'Sem Análise de Mérito' }
];
const getResultadoLabel = (valor: string) => {
return resultadoOptions.find(opt => opt.value === valor)?.label || valor;
};
const getResultadoColor = (resultado: string) => {
switch (resultado) {
case 'PROCEDENTE': return 'bg-green-100 text-green-800';
case 'PARCIALMENTE_PROCEDENTE': return 'bg-yellow-100 text-yellow-800';
case 'IMPROCEDENTE': return 'bg-red-100 text-red-800';
case 'SEM_ANALISE_MERITO': return 'bg-gray-100 text-gray-800';
default: return 'bg-blue-100 text-blue-800';
}
};
export const ProcessoView: React.FC = () => {
const { id } = useParams<{ id: string }>();
const { data: processo, refetch, isLoading, isError } = useQuery<Processo>(
['processo', id],
async () => {
const response = await api.get(`/processos/${id}`); // remover /api
return response.data;
}
);
if (isLoading) {
return <div>Carregando...</div>;
}
if (isError || !processo) {
return <div>Erro ao carregar processo. Processo não encontrado.</div>;
}
return (
<div className="space-y-6">
{/* Status e Resultado do Processo */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Status do Processo</h3>
{/* Botão de Julgamento */}
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-500">Processo julgado?</span>
<select
value={processo.resultado}
onChange={async (e) => {
try {
await api.patch(`/processos/${processo.id}/resultado`, {
resultado: e.target.value
});
refetch();
} catch (error) {
console.error('Erro ao atualizar resultado:', error);
alert('Erro ao atualizar resultado do processo');
}
}}
className="rounded-lg border-gray-300 text-sm dark:bg-gray-700 dark:border-gray-600"
>
{resultadoOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-500">Status:</span>
<span className="px-2 py-1 text-sm rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
{processo.status}
</span>
</div>
<div className="flex items-center space-x-2">
<span className="text-sm font-medium text-gray-500">Resultado:</span>
<span className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}>
{getResultadoLabel(processo.resultado)}
</span>
</div>
</div>
</div>
{/* Resto das informações do processo */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-4">
Processo {processo.numero}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h3 className="text-lg font-medium">Cliente</h3>
<p className="text-gray-700 dark:text-gray-300">{processo.cliente.nome}</p>
</div>
<div>
<h3 className="text-lg font-medium">Contraparte</h3>
<p className="text-gray-700 dark:text-gray-300">{processo.nomeContraparte}</p>
</div>
<div>
<h3 className="text-lg font-medium">Natureza</h3>
<p className="text-gray-700 dark:text-gray-300">{processo.natureza}</p>
</div>
<div>
<h3 className="text-lg font-medium">Status</h3>
<p className="text-gray-700 dark:text-gray-300">{processo.status}</p>
</div>
<div>
<h3 className="text-lg font-medium">Valor da Causa</h3>
<p className="text-gray-700 dark:text-gray-300">{processo.valorCausa?.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' })}</p>
</div>
<div>
<h3 className="text-lg font-medium">Data de Criação</h3>
<p className="text-gray-700 dark:text-gray-300">{format(new Date(processo.dataCriacao), 'dd/MM/yyyy', { locale: ptBR })}</p>
</div>
<div>
<h3 className="text-lg font-medium">Última Atualização</h3>
<p className="text-gray-700 dark:text-gray-300">{format(new Date(processo.dataAtualizacao), 'dd/MM/yyyy', { locale: ptBR })}</p>
</div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-medium mb-4">Resultado</h3>
<div className="flex items-center space-x-2">
<span className={`px-2 py-1 text-sm rounded-full ${getResultadoColor(processo.resultado)}`}>
{getResultadoLabel(processo.resultado)}
</span>
<select
value={processo.resultado}
onChange={async (e) => {
try {
await api.patch(`/processos/${processo.id}/resultado`, {
resultado: e.target.value
});
refetch(); // Atualizar dados do processo
} catch (error) {
console.error('Erro ao atualizar resultado:', error);
alert('Erro ao atualizar resultado do processo');
}
}}
className="rounded-lg border-gray-300 text-sm"
>
{resultadoOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
</div>
</div>
);
};
+176
View File
@@ -0,0 +1,176 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import { Processo } from "../../types/entities";
import { useProcessos } from "../../hooks/useProcessos";
import { formatNumeroProcesso } from "../../utils/formatters";
import { DocumentTextIcon, ClockIcon } from "@heroicons/react/24/outline";
interface ProcessosListProps {
searchTerm: string;
onEdit: (processo: Processo) => void;
}
const resultadoLabel = {
NAO_JULGADO: 'Não Julgado',
PROCEDENTE: 'Procedente',
PARCIALMENTE_PROCEDENTE: 'Parcialmente Procedente',
IMPROCEDENTE: 'Improcedente',
SEM_ANALISE_MERITO: 'Sem Análise de Mérito'
};
const resultadoColor = {
NAO_JULGADO: 'bg-gray-100 text-gray-800',
PROCEDENTE: 'bg-green-100 text-green-800',
PARCIALMENTE_PROCEDENTE: 'bg-yellow-100 text-yellow-800',
IMPROCEDENTE: 'bg-red-100 text-red-800',
SEM_ANALISE_MERITO: 'bg-gray-100 text-gray-800'
};
const getResultadoColor = (resultado: string) => {
return resultadoColor[resultado as keyof typeof resultadoColor] || 'bg-gray-100 text-gray-800';
};
export const ProcessosList: React.FC<ProcessosListProps> = ({
searchTerm,
onEdit,
}) => {
const navigate = useNavigate();
const { processos, isLoading } = useProcessos();
const filteredProcessos = processos.filter(
(processo) =>
processo.numero.includes(searchTerm) ||
processo.nomeContraparte
?.toLowerCase()
.includes(searchTerm.toLowerCase()),
);
const getResultadoDisplay = (processo: Processo) => {
if (!processo.resultado || !resultadoLabel[processo.resultado as keyof typeof resultadoLabel]) {
return (
<span className="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">
Não Julgado
</span>
);
}
return (
<span className={`px-2 py-1 text-xs rounded-full ${getResultadoColor(processo.resultado)}`}>
{resultadoLabel[processo.resultado as keyof typeof resultadoLabel]}
</span>
);
};
if (isLoading) {
return <div className="animate-pulse">Carregando...</div>;
}
return (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"
>
Processo
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"
>
Natureza
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"
>
Parte
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase"
>
Próximos Prazos
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
Resultado
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{filteredProcessos.map((processo) => (
<tr
key={processo.id}
onClick={() => navigate(`/processos/${processo.id}`)}
className="hover:bg-gray-50 cursor-pointer"
>
<td className="px-6 py-4">
<div className="flex items-center">
<DocumentTextIcon className="h-10 w-10 text-gray-400" />
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">
{formatNumeroProcesso(processo.numero)}
</div>
<div className="text-sm text-gray-500">
{processo.orgaoJulgador}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-500">
{processo.natureza}
</td>
<td className="px-6 py-4">
<div className="text-sm text-gray-900">
{processo.tipoParteCliente === "AUTOR" ? "Autor" : "Réu"}
</div>
<div className="text-sm text-gray-500">
{processo.nomeContraparte}
</div>
</td>
<td className="px-6 py-4">
<span
className={`px-2 py-1 text-xs rounded-full ${
processo.statusAtividade === "ATIVO"
? "bg-green-100 text-green-800"
: processo.statusAtividade === "BAIXADO"
? "bg-blue-100 text-blue-800"
: processo.statusAtividade === "SUSPENSO"
? "bg-yellow-100 text-yellow-800"
: "bg-gray-100 text-gray-800"
}`}
>
{processo.statusAtividade}
</span>
</td>
<td className="px-6 py-4">
{processo.diligencias?.length > 0 ? (
<div className="flex items-center text-sm text-gray-500">
<ClockIcon className="h-4 w-4 mr-1" />
{processo.diligencias.length} prazos
</div>
) : (
<span className="text-sm text-gray-500">Sem prazos</span>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{getResultadoDisplay(processo)}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
@@ -0,0 +1,74 @@
import React, { useState } from "react";
import { PlusIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { ProcessosList } from "./ProcessosList";
import { ProcessoModal } from "./ProcessoModal";
import { Processo } from "../../types/entities";
export const ProcessosPage: React.FC = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [selectedProcesso, setSelectedProcesso] = useState<Processo | null>(
null,
);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div className="relative">
<input
type="text"
placeholder="Buscar processos..."
className="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600
bg-white dark:bg-gray-700 text-gray-900 dark:text-white
focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<button
onClick={() => {
setSelectedProcesso(null);
setIsModalOpen(true);
}}
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg
hover:bg-blue-700 dark:hover:bg-blue-500 transition-colors"
>
<PlusIcon className="h-5 w-5 mr-2" />
Novo Processo
</button>
</div>
{/* Lista de Processos */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-900">
<tr>
{/* ...headers... */}
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{/* ...rows... */}
</tbody>
</table>
</div>
<ProcessosList
searchTerm={searchTerm}
onEdit={(processo) => {
setSelectedProcesso(processo);
setIsModalOpen(true);
}}
/>
<ProcessoModal
isOpen={isModalOpen}
processo={selectedProcesso}
onClose={() => {
setIsModalOpen(false);
setSelectedProcesso(null);
}}
/>
</div>
);
};
@@ -0,0 +1,262 @@
import {
Dialog,
TextField,
MenuItem,
Button,
Box,
Typography,
} from "@mui/material";
import { useState, useEffect } from "react";
import { TipoCustaProcessual, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
interface DialogCustaProcessualProps {
open: boolean;
onClose: () => void;
onSave: (data: any) => Promise<void>;
editingCusta?: any;
}
export function DialogCustaProcessual({
open,
onClose,
onSave,
editingCusta,
}: DialogCustaProcessualProps) {
const [formData, setFormData] = useState({
tipo: "",
descricao: "",
valor: "",
percentual: "",
status: "PENDENTE",
vencimento: null as Date | null,
dataPagamento: null as Date | null,
observacoes: "",
numeroParcelas: 1,
parcelaAtual: 1,
comprovante: "",
});
const [formError, setFormError] = useState<string | null>(null);
// Atualizar form quando o editingCusta muda
useEffect(() => {
if (editingCusta) {
setFormData({
tipo: editingCusta.tipo || "",
descricao: editingCusta.descricao || "",
valor: editingCusta.valor?.toString() || "",
percentual: editingCusta.percentual?.toString() || "",
status: editingCusta.status || "PENDENTE",
vencimento: editingCusta.vencimento
? new Date(editingCusta.vencimento)
: null,
dataPagamento: editingCusta.dataPagamento
? new Date(editingCusta.dataPagamento)
: null,
observacoes: editingCusta.observacoes || "",
numeroParcelas: editingCusta.numeroParcelas || 1,
parcelaAtual: editingCusta.parcelaAtual || 1,
comprovante: editingCusta.comprovante || "",
});
} else {
// Reset para os valores padrão
setFormData({
tipo: "",
descricao: "",
valor: "",
percentual: "",
status: "PENDENTE",
vencimento: null,
dataPagamento: null,
observacoes: "",
numeroParcelas: 1,
parcelaAtual: 1,
comprovante: "",
});
}
}, [editingCusta, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
// Validar campos obrigatórios
if (!formData.tipo) {
setFormError("Selecione o tipo da custa");
return;
}
if (!formData.descricao) {
setFormError("Informe a descrição da custa");
return;
}
if (!formData.valor) {
setFormError("Informe o valor da custa");
return;
}
// Preparar dados
const preparedData = {
...formData,
valor: Number(formData.valor),
percentual: formData.percentual ? Number(formData.percentual) : null,
numeroParcelas: Number(formData.numeroParcelas),
parcelaAtual: Number(formData.parcelaAtual),
};
await onSave(preparedData);
onClose();
} catch (error: any) {
console.error("Erro ao salvar custa:", error);
setFormError(error.message || "Erro ao salvar custa. Tente novamente.");
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<Box component="form" onSubmit={handleSubmit} sx={{ p: 3 }}>
<h2 className="text-lg font-semibold mb-4">
{editingCusta ? "Editar Custa Processual" : "Nova Custa Processual"}
</h2>
<div className="space-y-4">
<TextField
select
fullWidth
label="Tipo"
value={formData.tipo}
onChange={(e) => setFormData({ ...formData, tipo: e.target.value })}
required
>
{Object.values(TipoCustaProcessual).map((tipo) => (
<MenuItem key={tipo} value={tipo}>
{tipo.replace(/_/g, " ")}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="Descrição"
value={formData.descricao}
onChange={(e) =>
setFormData({ ...formData, descricao: e.target.value })
}
required
/>
<TextField
fullWidth
label="Valor"
type="number"
value={formData.valor}
onChange={(e) =>
setFormData({ ...formData, valor: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
<TextField
fullWidth
label="Percentual (se aplicável)"
type="number"
value={formData.percentual}
onChange={(e) =>
setFormData({ ...formData, percentual: e.target.value })
}
inputProps={{ step: "0.01", min: "0", max: "100" }}
/>
<LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={ptBR}
>
<DatePicker
label="Vencimento"
value={formData.vencimento}
onChange={(date) =>
setFormData({ ...formData, vencimento: date })
}
sx={{ width: "100%", mb: 2 }}
/>
{formData.status === "PAGO" && (
<DatePicker
label="Data de Pagamento"
value={formData.dataPagamento}
onChange={(date) =>
setFormData({ ...formData, dataPagamento: date })
}
sx={{ width: "100%" }}
/>
)}
</LocalizationProvider>
<TextField
select
fullWidth
label="Status"
value={formData.status}
onChange={(e) =>
setFormData({ ...formData, status: e.target.value })
}
required
>
{Object.values(StatusPagamento).map((status) => (
<MenuItem key={status} value={status}>
{status.replace(/_/g, " ")}
</MenuItem>
))}
</TextField>
<TextField
fullWidth
label="Número de Parcelas"
type="number"
value={formData.numeroParcelas}
onChange={(e) =>
setFormData({
...formData,
numeroParcelas: parseInt(e.target.value),
})
}
inputProps={{ min: 1 }}
/>
<TextField
fullWidth
label="Observações"
multiline
rows={3}
value={formData.observacoes}
onChange={(e) =>
setFormData({ ...formData, observacoes: e.target.value })
}
/>
</div>
{formError && (
<Box sx={{ mt: 2, p: 2, bgcolor: "error.light", borderRadius: 1 }}>
<Typography color="error">{formError}</Typography>
</Box>
)}
<Box
sx={{ mt: 3, display: "flex", justifyContent: "flex-end", gap: 2 }}
>
<Button onClick={onClose}>Cancelar</Button>
<Button type="submit" variant="contained" color="primary">
Salvar
</Button>
</Box>
</Box>
</Dialog>
);
}
export default DialogCustaProcessual;
@@ -0,0 +1,353 @@
import {
Dialog,
TextField,
MenuItem,
Button,
Box,
FormControl,
InputLabel,
Select,
Typography,
} from "@mui/material";
import { useState, useEffect } from "react";
import { FormaPagamento, StatusPagamento } from "@prisma/client";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import ptBR from "date-fns/locale/pt-BR";
import { IHonorario, IHonorarioCreate } from "../types/financeiro";
interface DialogHonorarioProps {
open: boolean;
onClose: () => void;
onSave: (data: Partial<IHonorarioCreate>) => Promise<void>;
editingHonorario?: IHonorario;
}
const DialogHonorario: React.FC<DialogHonorarioProps> = ({
open,
onClose,
onSave,
editingHonorario,
}) => {
const [formData, setFormData] = useState({
formaPagamento: "VISTA",
valorFixo: "",
percentualCausa: "",
percentualExito: "",
valorEntrada: "",
numeroParcelas: 1,
valorParcela: "",
diaVencimento: 10,
status: "PENDENTE",
dataVencimento: new Date(),
observacoes: "",
});
const [formError, setFormError] = useState<string | null>(null);
// Atualizar form quando o editingHonorario muda
useEffect(() => {
if (editingHonorario) {
setFormData({
formaPagamento: editingHonorario.formaPagamento,
valorFixo: editingHonorario.valorFixo || "",
percentualCausa: editingHonorario.percentualCausa || "",
percentualExito: editingHonorario.percentualExito || "",
valorEntrada: editingHonorario.valorEntrada || "",
numeroParcelas: editingHonorario.numeroParcelas || 1,
valorParcela: editingHonorario.valorParcela || "",
diaVencimento: editingHonorario.diaVencimento || 10,
status: editingHonorario.status,
dataVencimento: editingHonorario.dataVencimento
? new Date(editingHonorario.dataVencimento)
: new Date(),
observacoes: editingHonorario.observacoes || "",
});
} else {
// Reset para os valores padrão
setFormData({
formaPagamento: "VISTA",
valorFixo: "",
percentualCausa: "",
percentualExito: "",
valorEntrada: "",
numeroParcelas: 1,
valorParcela: "",
diaVencimento: 10,
status: "PENDENTE",
dataVencimento: new Date(),
observacoes: "",
});
}
}, [editingHonorario, open]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
// Validações básicas
const validationError = validateFormData(formData);
if (validationError) {
setFormError(validationError);
return;
}
// Converter strings numéricas
const preparedData = {
...formData,
valorFixo: formData.valorFixo ? Number(formData.valorFixo) : null,
percentualCausa: formData.percentualCausa
? Number(formData.percentualCausa)
: null,
percentualExito: formData.percentualExito
? Number(formData.percentualExito)
: null,
valorEntrada: formData.valorEntrada
? Number(formData.valorEntrada)
: null,
valorParcela: formData.valorParcela
? Number(formData.valorParcela)
: null,
};
await onSave(preparedData);
onClose();
} catch (error: any) {
console.error("Erro ao salvar honorário:", error);
setFormError(
error.message || "Erro ao salvar honorário. Tente novamente.",
);
}
};
const validateFormData = (data: any): string | null => {
if (!data.formaPagamento) {
return "Forma de pagamento é obrigatória";
}
switch (data.formaPagamento) {
case "VISTA":
if (!data.valorFixo) {
return "Informe o valor do honorário";
}
break;
case "PERCENTUAL_CAUSA":
if (!data.percentualCausa) {
return "Informe o percentual da causa";
}
break;
case "PERCENTUAL_EXITO":
if (!data.percentualExito) {
return "Informe o percentual do êxito";
}
break;
case "PARCELADO":
case "ENTRADA_MAIS_PARCELAS":
if (!data.valorParcela) {
return "Informe o valor da parcela";
}
if (!data.numeroParcelas || data.numeroParcelas < 1) {
return "Informe o número de parcelas";
}
break;
}
return null;
};
// Mostrar campos de entrada quando for percentual com entrada
const showEntrada =
formData.formaPagamento === "ENTRADA_MAIS_PARCELAS" ||
formData.formaPagamento === "PERCENTUAL_CAUSA_COM_ENTRADA";
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<Box component="form" onSubmit={handleSubmit} sx={{ p: 3 }}>
<h2 className="text-lg font-semibold mb-4">
{editingHonorario ? "Editar Honorário" : "Novo Honorário"}
</h2>
<div className="space-y-4">
<FormControl fullWidth>
<InputLabel>Forma de Pagamento</InputLabel>
<Select
value={formData.formaPagamento}
label="Forma de Pagamento"
onChange={(e) =>
setFormData({ ...formData, formaPagamento: e.target.value })
}
>
{Object.values(FormaPagamento).map((forma) => (
<MenuItem key={forma} value={forma}>
{forma.replace(/_/g, " ")}
</MenuItem>
))}
</Select>
</FormControl>
{formData.formaPagamento === "VISTA" && (
<TextField
fullWidth
label="Valor"
type="number"
value={formData.valorFixo}
onChange={(e) =>
setFormData({ ...formData, valorFixo: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
)}
{(formData.formaPagamento === "PERCENTUAL_CAUSA" ||
formData.formaPagamento === "PERCENTUAL_CAUSA_COM_ENTRADA") && (
<TextField
fullWidth
label="Percentual da Causa"
type="number"
value={formData.percentualCausa}
onChange={(e) =>
setFormData({ ...formData, percentualCausa: e.target.value })
}
required
inputProps={{ step: "0.01", min: "0", max: "100" }}
/>
)}
{formData.formaPagamento === "PERCENTUAL_EXITO" && (
<TextField
fullWidth
label="Percentual do Êxito"
type="number"
value={formData.percentualExito}
onChange={(e) =>
setFormData({ ...formData, percentualExito: e.target.value })
}
required
inputProps={{ step: "0.01", min: "0", max: "100" }}
/>
)}
{(formData.formaPagamento === "PARCELADO" ||
formData.formaPagamento === "ENTRADA_MAIS_PARCELAS") && (
<>
{showEntrada && (
<TextField
fullWidth
label="Valor da Entrada"
type="number"
value={formData.valorEntrada}
onChange={(e) =>
setFormData({ ...formData, valorEntrada: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
)}
<TextField
fullWidth
label="Número de Parcelas"
type="number"
value={formData.numeroParcelas}
onChange={(e) =>
setFormData({
...formData,
numeroParcelas: parseInt(e.target.value),
})
}
required
inputProps={{ min: "1" }}
/>
<TextField
fullWidth
label="Valor da Parcela"
type="number"
value={formData.valorParcela}
onChange={(e) =>
setFormData({ ...formData, valorParcela: e.target.value })
}
required
inputProps={{ step: "0.01" }}
/>
<TextField
fullWidth
label="Dia do Vencimento"
type="number"
value={formData.diaVencimento}
onChange={(e) =>
setFormData({
...formData,
diaVencimento: parseInt(e.target.value),
})
}
inputProps={{ min: 1, max: 31 }}
required
/>
</>
)}
<TextField
select
fullWidth
label="Status"
value={formData.status}
onChange={(e) =>
setFormData({ ...formData, status: e.target.value })
}
required
>
{Object.values(StatusPagamento).map((status) => (
<MenuItem key={status} value={status}>
{status.replace(/_/g, " ")}
</MenuItem>
))}
</TextField>
<LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={ptBR}
>
<DatePicker
label="Data de Vencimento"
value={formData.dataVencimento}
onChange={(date) =>
setFormData({ ...formData, dataVencimento: date || new Date() })
}
sx={{ width: "100%" }}
/>
</LocalizationProvider>
<TextField
fullWidth
label="Observações"
multiline
rows={3}
value={formData.observacoes}
onChange={(e) =>
setFormData({ ...formData, observacoes: e.target.value })
}
/>
</div>
{formError && (
<Box sx={{ mt: 2, p: 2, bgcolor: "error.light", borderRadius: 1 }}>
<Typography color="error">{formError}</Typography>
</Box>
)}
<Box
sx={{ mt: 3, display: "flex", justifyContent: "flex-end", gap: 2 }}
>
<Button onClick={onClose}>Cancelar</Button>
<Button type="submit" variant="contained" color="primary">
Salvar
</Button>
</Box>
</Box>
</Dialog>
);
};
export default DialogHonorario;
@@ -0,0 +1,213 @@
import { Fragment, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { DatePicker } from "@mui/x-date-pickers";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import ptBR from "date-fns/locale/pt-BR";
import { formatCurrency } from "../../../utils/format";
import { StatusPagamento } from "@prisma/client";
interface ParcelaHonorario {
id: string;
honorarioId: string;
numeroParcela: number;
valor: number;
valorPago?: number | null;
vencimento: Date;
dataPagamento?: Date | null;
status: StatusPagamento;
comprovante?: string | null;
observacoes?: string | null;
}
interface DialogParcelaProps {
isOpen: boolean;
onClose: () => void;
onSave: (data: any) => Promise<void>;
parcela: ParcelaHonorario;
tipo: "honorario" | "custa";
}
export function DialogParcela({
isOpen,
onClose,
onSave,
parcela,
tipo,
}: DialogParcelaProps) {
const [formData, setFormData] = useState({
status: parcela?.status || "PENDENTE",
dataPagamento: parcela?.dataPagamento
? new Date(parcela?.dataPagamento)
: null,
valorPago: parcela?.valorPago || "",
comprovante: parcela?.comprovante || "",
observacoes: parcela?.observacoes || "",
});
const handleValorPagoChange = (valor: string) => {
const valorPago = Number(valor);
const valorParcela = Number(parcela.valor);
let novoStatus = "PENDENTE";
if (valorPago >= valorParcela) {
novoStatus = "PAGO";
} else if (valorPago > 0) {
novoStatus = "PARCIALMENTE_PAGO";
}
setFormData((prev) => ({
...prev,
valorPago: valor,
status: novoStatus,
dataPagamento: novoStatus === "PAGO" ? new Date() : prev.dataPagamento,
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await onSave(formData);
onClose();
};
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={onClose}>
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm" />
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4">
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="div"
className="flex items-center justify-between mb-4"
>
<h3 className="text-lg font-medium leading-6 text-gray-900">
Atualizar Parcela {parcela?.numeroParcela}
</h3>
<button
type="button"
className="text-gray-400 hover:text-gray-500"
onClick={onClose}
>
<XMarkIcon className="h-6 w-6" />
</button>
</Dialog.Title>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">
Valor da Parcela
</label>
<input
type="text"
value={formatCurrency(parcela.valor)}
disabled
className="mt-1 block w-full rounded-md border-gray-300 bg-gray-50"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Valor Pago
</label>
<input
type="number"
step="0.01"
value={formData.valorPago}
onChange={(e) => handleValorPagoChange(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Status
</label>
<select
value={formData.status}
onChange={(e) =>
setFormData({ ...formData, status: e.target.value })
}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
>
{Object.values(StatusPagamento).map((status) => (
<option key={status} value={status}>
{status.replace(/_/g, " ")}
</option>
))}
</select>
</div>
{formData.status === "PAGO" && (
<div>
<label className="block text-sm font-medium text-gray-700">
Data do Pagamento
</label>
<LocalizationProvider
dateAdapter={AdapterDateFns}
adapterLocale={ptBR}
>
<DatePicker
value={formData.dataPagamento}
onChange={(date) =>
setFormData({ ...formData, dataPagamento: date })
}
className="mt-1 block w-full"
/>
</LocalizationProvider>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700">
Comprovante (URL)
</label>
<input
type="text"
value={formData.comprovante}
onChange={(e) =>
setFormData({ ...formData, comprovante: e.target.value })
}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Observações
</label>
<textarea
value={formData.observacoes}
onChange={(e) =>
setFormData({ ...formData, observacoes: e.target.value })
}
rows={3}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
/>
</div>
<div className="mt-6 flex justify-end space-x-3">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2"
>
Cancelar
</button>
<button
type="submit"
className="inline-flex justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
Salvar
</button>
</div>
</form>
</Dialog.Panel>
</div>
</div>
</Dialog>
</Transition>
);
}
@@ -0,0 +1,3 @@
export { default as DialogCustaProcessual } from "./DialogCustaProcessual";
export { default as DialogHonorario } from "./DialogHonorario";
export { default as DialogParcela } from "./DialogParcela";
@@ -0,0 +1,53 @@
import { memo } from "react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableRow,
Chip,
IconButton,
} from "@mui/material";
import { Edit as EditIcon } from "@mui/icons-material";
import { formatCurrency, formatDate } from "../../../utils/format";
const CustasTable = memo(({ custas, onEdit, getStatusColor }) => {
return (
<Table>
<TableHead>
<TableRow>
<TableCell>Tipo</TableCell>
<TableCell>Descrição</TableCell>
<TableCell align="right">Valor</TableCell>
<TableCell>Vencimento</TableCell>
<TableCell>Status</TableCell>
<TableCell>Ações</TableCell>
</TableRow>
</TableHead>
<TableBody>
{custas?.map((custa) => (
<TableRow key={custa.id}>
<TableCell>{custa.tipo}</TableCell>
<TableCell>{custa.descricao}</TableCell>
<TableCell align="right">{formatCurrency(custa.valor)}</TableCell>
<TableCell>{formatDate(custa.vencimento)}</TableCell>
<TableCell>
<Chip
label={custa.status}
color={getStatusColor(custa.status)}
size="small"
/>
</TableCell>
<TableCell>
<IconButton size="small" onClick={() => onEdit(custa)}>
<EditIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
});
export default CustasTable;