first commit
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user