first commit

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

No files matched your search

@@ -0,0 +1,251 @@
import { useState } from 'react';
import { useQuery, useQueryClient } from 'react-query';
import { formatCurrency } from '../../utils/format';
import { Dialog } from '@headlessui/react';
import { useForm } from 'react-hook-form';
import { FormInput, FormSelect } from '../forms';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
import api from '../../services/api';
// Função auxiliar para retornar as cores do status
const getStatusColor = (status: string) => {
const colors = {
PENDENTE: 'bg-yellow-100 text-yellow-800',
PAGO: 'bg-green-100 text-green-800',
VENCIDO: 'bg-red-100 text-red-800',
CANCELADO: 'bg-gray-100 text-gray-800',
PARCIALMENTE_PAGO: 'bg-blue-100 text-blue-800'
};
return colors[status] || colors.PENDENTE;
};
const TIPOS_CUSTA = [
{ value: 'TAXA_ADMINISTRATIVA', label: 'Taxa Administrativa' },
{ value: 'RECURSO', label: 'Recurso' },
{ value: 'DILIGENCIA', label: 'Diligência' },
{ value: 'CERTIDAO', label: 'Certidão' },
{ value: 'COPIA', label: 'Cópia' },
{ value: 'OUTROS', label: 'Outros' }
];
export function CustaAdministrativaPanel({ processoId }) {
const [isModalOpen, setIsModalOpen] = useState(false);
const [selectedCusta, setSelectedCusta] = useState(null);
const { register, handleSubmit, reset, setValue } = useForm();
const queryClient = useQueryClient();
const { data: custas, isLoading } = useQuery(
['custas-administrativas', processoId],
async () => {
const response = await api.get(`/processos-administrativos/${processoId}/custas`);
return response.data;
}
);
const handleEdit = (custa) => {
setSelectedCusta(custa);
Object.keys(custa).forEach(key => {
if (key === 'vencimento' || key === 'dataPagamento') {
setValue(key, custa[key] ? new Date(custa[key]).toISOString().split('T')[0] : '');
} else {
setValue(key, custa[key]);
}
});
setIsModalOpen(true);
};
const handleDelete = async (id) => {
if (window.confirm('Tem certeza que deseja excluir esta custa?')) {
try {
await api.delete(`/processos-administrativos/${processoId}/custas/${id}`);
queryClient.invalidateQueries(['custas-administrativas', processoId]);
} catch (error) {
console.error('Erro ao excluir custa:', error);
}
}
};
const onSubmit = async (data) => {
try {
// Formatar as datas antes de enviar
const formattedData = {
...data,
vencimento: data.vencimento ? new Date(data.vencimento).toISOString() : null,
dataPagamento: data.dataPagamento ? new Date(data.dataPagamento).toISOString() : null,
valor: Number(data.valor)
};
if (selectedCusta) {
await api.put(`/processos-administrativos/${processoId}/custas/${selectedCusta.id}`, formattedData);
} else {
await api.post(`/processos-administrativos/${processoId}/custas`, formattedData);
}
queryClient.invalidateQueries(['custas-administrativas', processoId]);
setIsModalOpen(false);
setSelectedCusta(null);
reset();
} catch (error) {
console.error('Erro ao salvar custa:', error);
}
};
if (isLoading) return <div>Carregando...</div>;
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-medium">Custas e Despesas</h3>
<button
onClick={() => setIsModalOpen(true)}
className="px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
>
Nova Custa
</button>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Tipo</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Descrição</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Valor</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Vencimento</th>
<th className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Ações</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{custas?.map((custa) => (
<tr key={custa.id}>
<td className="px-3 py-4 text-sm text-gray-900">{custa.tipo}</td>
<td className="px-3 py-4 text-sm text-gray-500">{custa.descricao}</td>
<td className="px-3 py-4 text-sm text-gray-900">{formatCurrency(custa.valor)}</td>
<td className="px-3 py-4 text-sm">
<span className={`px-2 py-1 rounded-full text-xs ${getStatusColor(custa.status)}`}>
{custa.status}
</span>
</td>
<td className="px-3 py-4 text-sm text-gray-500">
{custa.vencimento ? new Date(custa.vencimento).toLocaleDateString() : '-'}
</td>
<td className="px-3 py-4 text-sm">
<div className="flex space-x-2">
<button
onClick={() => handleEdit(custa)}
className="text-blue-600 hover:text-blue-800"
>
<PencilIcon className="h-4 w-4" />
</button>
<button
onClick={() => handleDelete(custa.id)}
className="text-red-600 hover:text-red-800"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Modal de Custa */}
<Dialog open={isModalOpen} onClose={() => {
setIsModalOpen(false);
setSelectedCusta(null);
reset();
}}>
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="mx-auto max-w-2xl w-full bg-white rounded-xl p-6">
<Dialog.Title className="text-lg font-semibold mb-4">
{selectedCusta ? 'Editar Custa' : 'Nova Custa'}
</Dialog.Title>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormSelect
label="Tipo"
required
{...register("tipo")}
options={TIPOS_CUSTA}
/>
<FormInput
label="Valor"
type="number"
step="0.01"
required
{...register("valor")}
/>
<div className="col-span-2">
<FormInput
label="Descrição"
required
{...register("descricao")}
/>
</div>
<FormSelect
label="Status"
{...register("status")}
options={[
{ value: 'PENDENTE', label: 'Pendente' },
{ value: 'PAGO', label: 'Pago' },
{ value: 'VENCIDO', label: 'Vencido' },
{ value: 'CANCELADO', label: 'Cancelado' }
]}
/>
<FormInput
label="Vencimento"
type="date"
{...register("vencimento")}
/>
<FormInput
label="Data Pagamento"
type="date"
{...register("dataPagamento")}
/>
<div className="col-span-2">
<FormInput
label="Observações"
multiline
{...register("observacoes")}
/>
</div>
</div>
<div className="flex justify-end space-x-2">
<button
type="button"
onClick={() => {
setIsModalOpen(false);
setSelectedCusta(null);
reset();
}}
className="px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 rounded-md"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md"
>
Salvar
</button>
</div>
</form>
</Dialog.Panel>
</div>
</Dialog>
</div>
);
}
@@ -0,0 +1,57 @@
import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/outline';
import { Fragment } from 'react';
import { MovimentacaoAdministrativa } from '../../types/processoAdministrativo';
interface MovimentacaoAdminActionsProps {
movimentacao: MovimentacaoAdministrativa;
onEdit: (movimentacao: MovimentacaoAdministrativa) => void;
onDelete: (id: string) => void;
}
export function MovimentacaoAdminActions({ movimentacao, onEdit, onDelete }: MovimentacaoAdminActionsProps) {
return (
<Menu as="div" className="relative">
<Menu.Button className="p-1 rounded-full hover:bg-gray-100">
<EllipsisVerticalIcon className="h-5 w-5 text-gray-400" />
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Menu.Item>
{({ active }) => (
<button
onClick={() => onEdit(movimentacao)}
className={`${
active ? 'bg-gray-100' : ''
} block px-4 py-2 text-sm text-gray-700 w-full text-left`}
>
Editar
</button>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<button
onClick={() => onDelete(movimentacao.id)}
className={`${
active ? 'bg-gray-100' : ''
} block px-4 py-2 text-sm text-red-600 w-full text-left`}
>
Excluir
</button>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
}
@@ -0,0 +1,202 @@
import { Dialog } from "@headlessui/react";
import { useState, useEffect } from "react";
import { MovimentacaoAdministrativa } from "../../types/processoAdministrativo";
interface MovimentacaoAdministrativaDialogProps {
isOpen: boolean;
onClose: () => void;
onSave: (movimentacao: Partial<MovimentacaoAdministrativa>) => Promise<void>;
movimentacao?: MovimentacaoAdministrativa | null;
}
const TIPOS_MOVIMENTACAO = [
'DESPACHO',
'DECISÃO',
'NOTIFICAÇÃO',
'MANIFESTAÇÃO',
'RECURSO',
'OUTROS'
] as const;
export function MovimentacaoAdministrativaDialog({
isOpen,
onClose,
onSave,
movimentacao
}: MovimentacaoAdministrativaDialogProps) {
const [formData, setFormData] = useState({
data: new Date(),
descricao: '',
tipoMovimentacao: '',
numero: '',
documentos: [] as string[],
observacoes: ''
});
useEffect(() => {
if (movimentacao) {
setFormData({
...movimentacao,
data: new Date(movimentacao.data)
});
} else {
setFormData({
data: new Date(),
descricao: '',
tipoMovimentacao: '',
numero: '',
documentos: [],
observacoes: ''
});
}
}, [movimentacao]);
const [formError, setFormError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormError(null);
try {
// Validações
if (!formData.descricao) {
setFormError('A descrição é obrigatória');
return;
}
if (!formData.tipoMovimentacao) {
setFormError('O tipo de movimentação é obrigatório');
return;
}
await onSave(formData);
onClose();
} catch (error) {
console.error('Erro ao salvar movimentação:', error);
setFormError('Erro ao salvar movimentação. Tente novamente.');
}
};
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="mx-auto max-w-lg rounded-lg bg-white p-6">
<Dialog.Title className="text-lg font-medium mb-4">
Nova Movimentação
</Dialog.Title>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Data
</label>
<input
type="datetime-local"
className="w-full rounded-lg border-gray-300"
value={formData.data.toISOString().slice(0, 16)}
onChange={(e) => setFormData({
...formData,
data: new Date(e.target.value)
})}
required
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Tipo de Movimentação
</label>
<select
className="w-full rounded-lg border-gray-300"
value={formData.tipoMovimentacao}
onChange={(e) => setFormData({
...formData,
tipoMovimentacao: e.target.value
})}
required
>
<option value="">Selecione...</option>
{TIPOS_MOVIMENTACAO.map((tipo) => (
<option key={tipo} value={tipo}>
{tipo}
</option>
))}
</select>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Número/Protocolo (opcional)
</label>
<input
type="text"
className="w-full rounded-lg border-gray-300"
value={formData.numero}
onChange={(e) => setFormData({
...formData,
numero: e.target.value
})}
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Descrição
</label>
<textarea
className="w-full rounded-lg border-gray-300"
rows={3}
value={formData.descricao}
onChange={(e) => setFormData({
...formData,
descricao: e.target.value
})}
required
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Observações
</label>
<textarea
className="w-full rounded-lg border-gray-300"
rows={2}
value={formData.observacoes}
onChange={(e) => setFormData({
...formData,
observacoes: e.target.value
})}
/>
</div>
</div>
{formError && (
<div className="text-sm text-red-600 bg-red-50 p-2 rounded">
{formError}
</div>
)}
<div className="flex justify-end gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-700 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>
);
}
@@ -0,0 +1,309 @@
import { useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { useQuery, useQueryClient } from "react-query";
import { Tab } from "@headlessui/react";
import { classNames } from "../../utils/classNames";
import {
ArrowLeftIcon,
DocumentTextIcon,
ClockIcon,
PencilIcon,
TrashIcon,
CurrencyDollarIcon,
} from "@heroicons/react/24/outline";
import { formatCurrency } from "../../utils/format";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import { ProcessoAdminModal } from "./ProcessoAdminModal";
import { MovimentacaoAdministrativaDialog } from "./MovimentacaoAdministrativaDialog";
import { MovimentacaoAdminActions } from "./MovimentacaoAdminActions"; // Adicionar esta importação
import api from "../../services/api";
import { CustaAdministrativaPanel } from "./CustaAdministrativaPanel";
const tabs = [
{ id: "geral", name: "Informações Gerais", icon: DocumentTextIcon },
{ id: "movimentacoes", name: "Movimentações", icon: ClockIcon },
{ id: "financeiro", name: "Financeiro", icon: CurrencyDollarIcon }, // Adicionar nova tab
];
export function ProcessoAdminDetalhes() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [selectedTab, setSelectedTab] = useState(0);
const [isMovimentacaoModalOpen, setIsMovimentacaoModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [movimentacaoParaEditar, setMovimentacaoParaEditar] = useState<MovimentacaoAdministrativa | null>(null);
// Se o id for "novo", renderizar diretamente o modal de criação
if (id === "novo") {
return (
<ProcessoAdminModal
isOpen={true}
onClose={() => navigate("/processos-administrativos")}
onSuccess={() => navigate("/processos-administrativos")}
/>
);
}
const { data: processo, isLoading, error } = useQuery(
["processo-administrativo", id],
async () => {
const response = await api.get(`/processos-administrativos/${id}`);
return response.data;
},
{
enabled: !!id && id !== "novo", // Só executa se tiver um id válido
retry: 2, // Tenta no máximo 2 vezes em caso de erro
onError: (error) => {
console.error("Erro ao carregar processo:", error);
}
}
);
const { deleteProcesso } = useProcessosAdministrativos();
const handleDelete = async () => {
try {
await deleteProcesso.mutateAsync(id!);
navigate("/processos-administrativos");
} catch (error) {
console.error("Erro ao excluir processo:", error);
}
};
const handleSaveMovimentacao = async (data: any) => {
try {
await api.post(`/processos-administrativos/${id}/movimentacoes`, data);
// Recarregar dados
queryClient.invalidateQueries(["processo-administrativo", id]);
} catch (error) {
console.error("Erro ao salvar movimentação:", error);
}
};
const handleEditMovimentacao = async (data: any) => {
try {
// Atualizar URL para usar a nova rota
await api.put(`/processos-administrativos/movimentacoes/${data.id}`, data);
queryClient.invalidateQueries(["processo-administrativo", id]);
setMovimentacaoParaEditar(null);
} catch (error) {
console.error("Erro ao editar movimentação:", error);
}
};
const handleDeleteMovimentacao = async (movimentacaoId: string) => {
if (!window.confirm("Deseja realmente excluir esta movimentação?")) return;
try {
// Atualizar URL para usar a nova rota
await api.delete(`/processos-administrativos/movimentacoes/${movimentacaoId}`);
queryClient.invalidateQueries(["processo-administrativo", id]);
} catch (error) {
console.error("Erro ao excluir movimentação:", error);
}
};
if (!processo) return <div>Processo não encontrado</div>;
if (isLoading) return <div>Carregando...</div>;
if (error) return <div>Erro ao carregar processo</div>;
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-administrativos")}
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">
{processo.numero}
</h1>
<p className="text-sm text-gray-500">{processo.orgao}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<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
</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>
</div>
{/* Tabs */}
<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-4">
{/* 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/Protocolo
</dt>
<dd className="mt-1 text-sm text-gray-900">{processo.numero}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Órgão</dt>
<dd className="mt-1 text-sm text-gray-900">{processo.orgao}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">Status</dt>
<dd className="mt-1 text-sm text-gray-900">{processo.status}</dd>
</div>
<div>
<dt className="text-sm font-medium text-gray-500">
Valor da Pretensão
</dt>
<dd className="mt-1 text-sm text-gray-900">
{processo.valorPretensao
? formatCurrency(processo.valorPretensao)
: "-"}
</dd>
</div>
<div className="col-span-2">
<dt className="text-sm font-medium text-gray-500">Descrição</dt>
<dd className="mt-1 text-sm text-gray-900">
{processo.descricao}
</dd>
</div>
<div className="col-span-2">
<dt className="text-sm font-medium text-gray-500">Observações</dt>
<dd className="mt-1 text-sm text-gray-900">
{processo.observacoes || "-"}
</dd>
</div>
</dl>
</Tab.Panel>
{/* Movimentações */}
<Tab.Panel className="bg-white rounded-xl p-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium text-gray-900">Movimentações</h3>
<button
onClick={() => setIsMovimentacaoModalOpen(true)}
className="flex items-center px-4 py-2 text-sm text-blue-600 hover:bg-blue-50 rounded-lg"
>
Nova Movimentação
</button>
</div>
{processo.movimentacoes?.length > 0 ? (
<div className="flow-root">
<ul role="list" className="-mb-8">
{processo.movimentacoes.map((mov, idx) => (
<li key={mov.id}>
<div className="relative pb-8">
{idx !== processo.movimentacoes.length - 1 ? (
<span
className="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200"
aria-hidden="true"
/>
) : null}
<div className="relative flex space-x-3">
<div>
<span className="h-8 w-8 rounded-full bg-gray-100 flex items-center justify-center">
<ClockIcon className="h-5 w-5 text-gray-500" />
</span>
</div>
<div className="min-w-0 flex-1">
<div className="flex justify-between items-start">
<div className="text-sm text-gray-500">
<time>{new Date(mov.data).toLocaleDateString()}</time>
</div>
<MovimentacaoAdminActions
movimentacao={mov}
onEdit={setMovimentacaoParaEditar}
onDelete={handleDeleteMovimentacao}
/>
</div>
<div className="mt-1">
<p className="text-sm text-gray-900">
{mov.descricao}
</p>
{mov.observacoes && (
<p className="mt-1 text-sm text-gray-500">
{mov.observacoes}
</p>
)}
</div>
</div>
</div>
</div>
</li>
))}
</ul>
</div>
) : (
<p className="text-sm text-gray-500 text-center py-4">
Nenhuma movimentação registrada
</p>
)}
</Tab.Panel>
{/* Painel Financeiro */}
<Tab.Panel className="bg-white rounded-xl p-6">
<CustaAdministrativaPanel processoId={id} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
{/* Modais */}
<ProcessoAdminModal
isOpen={isEditModalOpen}
onClose={() => setIsEditModalOpen(false)}
processo={processo}
onSuccess={() => {
queryClient.invalidateQueries(["processo-administrativo", id]);
setIsEditModalOpen(false);
}}
/>
<MovimentacaoAdministrativaDialog
isOpen={isMovimentacaoModalOpen}
onClose={() => setIsMovimentacaoModalOpen(false)}
onSave={handleSaveMovimentacao}
/>
<MovimentacaoAdministrativaDialog
isOpen={!!movimentacaoParaEditar}
onClose={() => setMovimentacaoParaEditar(null)}
onSave={handleEditMovimentacao}
movimentacao={movimentacaoParaEditar}
/>
</div>
);
}
@@ -0,0 +1,175 @@
import { Dialog } from "@headlessui/react";
import { useForm } from "react-hook-form";
import { ProcessoAdministrativo, StatusProcessoAdministrativo } from "../../types/processoAdministrativo";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import { useClientes } from "../../hooks/useClientes";
import { FormInput, FormSelect } from "../forms";
import { TipoParte } from "../../types/entities";
import { useState, useEffect } from "react";
interface ProcessoAdminModalProps {
isOpen: boolean;
onClose: () => void;
processo?: ProcessoAdministrativo;
onSuccess?: () => void;
}
export function ProcessoAdminModal({
isOpen,
onClose,
processo,
onSuccess
}: ProcessoAdminModalProps) {
const { register, handleSubmit, setValue, reset } = useForm();
const { createProcesso, updateProcesso } = useProcessosAdministrativos();
const { clientes } = useClientes();
const [clienteSearch, setClienteSearch] = useState("");
// Reset form quando modal abrir/fechar
useEffect(() => {
if (isOpen && processo) {
Object.keys(processo).forEach((key) => {
if (key !== "id" && key !== "dataCriacao" && key !== "dataAtualizacao") {
setValue(key, processo[key]);
}
});
} else {
reset({
numero: "",
descricao: "",
orgao: "",
clienteId: "",
tipoParteCliente: TipoParte.AUTOR,
valorPretensao: "",
status: StatusProcessoAdministrativo.INICIAL,
observacoes: "",
});
}
}, [isOpen, processo, setValue, reset]);
const onSubmit = async (data: any) => {
try {
console.log("Dados do formulário:", data); // Adicionar log
const processData = {
...data,
valorPretensao: data.valorPretensao ? Number(data.valorPretensao) : null,
};
if (processo) {
await updateProcesso.mutateAsync({
id: processo.id,
data: processData
});
} else {
await createProcesso.mutateAsync(processData);
}
onSuccess?.();
onClose();
} catch (error) {
console.error("Erro detalhado ao salvar processo:", error); // Melhorar log de erro
}
};
return (
<Dialog open={isOpen} onClose={onClose} className="relative z-50">
<div className="fixed inset-0 bg-black/30" aria-hidden="true" />
<div className="fixed inset-0 flex items-center justify-center p-4">
<Dialog.Panel className="mx-auto max-w-2xl w-full bg-white rounded-xl p-6">
<Dialog.Title className="text-lg font-semibold mb-4">
{processo ? "Editar Processo Administrativo" : "Novo Processo Administrativo"}
</Dialog.Title>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormInput
label="Número/Protocolo"
required
{...register("numero")}
/>
<FormSelect
label="Cliente"
required
{...register("clienteId")}
options={clientes.map(c => ({
value: c.id,
label: `${c.nome} (${c.cpfCnpj})`
}))}
/>
<FormSelect
label="Tipo de Parte"
required
{...register("tipoParteCliente")}
options={[
{ value: TipoParte.AUTOR, label: "Autor" },
{ value: TipoParte.REU, label: "Réu" }
]}
/>
<FormInput
label="Órgão"
required
{...register("orgao")}
/>
<div className="col-span-2">
<FormInput
label="Descrição"
required
multiline
rows={3}
{...register("descricao")}
/>
</div>
<FormInput
label="Valor da Pretensão"
type="number"
step="0.01"
{...register("valorPretensao")}
/>
<FormSelect
label="Status"
required
{...register("status")}
options={Object.values(StatusProcessoAdministrativo).map(status => ({
value: status,
label: status.replace(/_/g, " ")
}))}
/>
<div className="col-span-2">
<FormInput
label="Observações"
multiline
rows={3}
{...register("observacoes")}
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-4">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-700 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>
);
}
@@ -0,0 +1,100 @@
import { useNavigate } from "react-router-dom";
import { DocumentTextIcon } from "@heroicons/react/24/outline";
import { useProcessosAdministrativos } from "../../hooks/useProcessosAdministrativos";
import { formatCurrency } from "../../utils/format";
import { ProcessoAdministrativo, StatusProcessoAdministrativo } from "../../types/processoAdministrativo";
const statusColor = {
[StatusProcessoAdministrativo.INICIAL]: 'bg-yellow-100 text-yellow-800',
[StatusProcessoAdministrativo.EM_ANALISE]: 'bg-blue-100 text-blue-800',
[StatusProcessoAdministrativo.EM_RECURSO]: 'bg-purple-100 text-purple-800',
[StatusProcessoAdministrativo.FINALIZADO]: 'bg-green-100 text-green-800',
[StatusProcessoAdministrativo.ARQUIVADO]: 'bg-gray-100 text-gray-800'
};
export function ProcessoAdministrativoList() {
console.log('Renderizando ProcessoAdministrativoList');
const navigate = useNavigate();
const { processos, isLoading } = useProcessosAdministrativos();
if (isLoading) {
return <div className="p-4">Carregando...</div>;
}
return (
<div className="px-4 sm:px-6 lg:px-8">
<div className="sm:flex sm:items-center">
<div className="sm:flex-auto">
<h1 className="text-xl font-semibold text-gray-900">Processos Administrativos</h1>
</div>
<div className="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<button
type="button"
onClick={() => navigate("/processos-administrativos/novo")}
className="inline-flex items-center justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700"
>
Novo Processo
</button>
</div>
</div>
<div className="mt-8 flex flex-col">
<div className="-mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full divide-y divide-gray-300">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Número/Protocolo
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Órgão
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Status
</th>
<th scope="col" className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">
Valor Pretensão
</th>
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span className="sr-only">Ações</span>
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 bg-white">
{processos?.map((processo) => (
<tr key={processo.id}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-900">
{processo.numero}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{processo.orgao}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<span className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${statusColor[processo.status]}`}>
{processo.status}
</span>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{processo.valorPretensao ? formatCurrency(processo.valorPretensao) : '-'}
</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
<button
onClick={() => navigate(`/processos-administrativos/${processo.id}`)}
className="text-blue-600 hover:text-blue-900"
>
Visualizar
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}