Files
advx/src/components/calculos/DetalhesCalculo.tsx
T
rodrigo 19f23a540a calculo: fix pensaoalimenticia
Signed-off-by: Rodrigo B. de Sousa Martins <rodrigo.sousa.577@gmail.com>
2025-07-16 16:52:50 -03:00

2010 lines
80 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { format } from 'date-fns';
import ptBR from 'date-fns/locale/pt-BR';
import { toast } from 'react-toastify';
import { Card, Button, Tabs, TabList, Tab, TabPanel } from '../ui';
import { useAuth } from '../../contexts/AuthContext';
import { api } from '../../services/api';
import axios from 'axios';
import { formatarData, formatarMoeda } from '../../utils/formatters';
import { PencilIcon, TrashIcon } from '@heroicons/react/24/outline';
interface Calculo {
id: string;
titulo: string;
descricao: string;
status: string;
dataBase: string;
dataCalculo: string;
valorBase: string;
valorCorrigido: string;
valorFinal: string;
usuario: {
id: string;
nome: string;
};
processo?: {
id: string;
numero: string;
titulo: string;
};
cliente?: {
id: string;
nome: string;
};
correcoes: any[];
juros: any[];
multas: any[];
honorarios: any[];
parcelas: any[];
descontos: any[];
historico: any[];
}
interface CorrecaoForm {
indice: string;
dataInicial: string;
dataFinal: string;
valorOriginal: string;
}
export function DetalhesCalculo() {
const params = useParams<{ id: string }>();
const calculoId = params.id; // Obter ID da URL
const navigate = useNavigate();
const { getToken } = useAuth();
const [calculo, setCalculo] = useState<Calculo | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState('resumo');
const [formData, setFormData] = useState<CorrecaoForm>({
indice: 'INPC',
dataInicial: '',
dataFinal: '',
valorOriginal: ''
});
const loadCalculo = async () => {
if (!calculoId) return;
try {
setLoading(true);
setError(null);
const response = await api.get(`/calculos/${calculoId}`);
setCalculo(response.data);
} catch (error) {
console.error('Erro ao carregar cálculo:', error);
setError('Não foi possível carregar os dados do cálculo');
toast.error('Erro ao carregar dados do cálculo');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadCalculo();
}, [calculoId]);
const finalizarCalculo = async () => {
if (!calculoId || !calculo) return;
try {
// Obter token de autenticação
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado. Faça login novamente.');
}
// Usar o serviço api em vez de fetch direto
await api.post(`/calculos/${calculoId}/finalizar`, {}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
toast.success('Cálculo finalizado com sucesso!');
loadCalculo(); // Recarregar os dados
} catch (error) {
console.error('Erro ao finalizar cálculo:', error);
toast.error(`Falha ao finalizar cálculo: ${error.message}`);
}
};
const gerarRelatorio = async () => {
try {
const response = await api.get(`/calculos/${calculoId}/relatorio`, {
responseType: 'blob' // Importante para receber o PDF como blob
});
// Criar URL do blob e abrir em uma nova aba
const blob = new Blob([response.data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
window.open(url, '_blank');
} catch (error) {
console.error('Erro ao gerar relatório:', error);
toast.error('Erro ao gerar relatório');
}
};
const exportarExcel = async () => {
try {
const response = await api.get(`/calculos/${calculoId}/excel`, {
responseType: 'blob' // Importante para receber o Excel como blob
});
// Criar URL do blob e forçar download
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
// Criar link temporário para download
const link = document.createElement('a');
link.href = url;
link.download = `calculo_${calculoId}.xlsx`;
link.click();
// Limpar URL
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Erro ao exportar Excel:', error);
toast.error('Erro ao exportar para Excel');
}
};
// Função para formatar valores monetários
const formatarValor = (valor: string) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(parseFloat(valor));
};
// Função para formatar datas
const formatarData = (data: string) => {
try {
return format(new Date(data), 'dd/MM/yyyy', { locale: ptBR });
} catch (error) {
return 'Data inválida';
}
};
// Status Badge
const StatusBadge = ({ status }: { status: string }) => {
const getStatusColor = () => {
switch (status) {
case 'RASCUNHO':
return 'bg-yellow-100 text-yellow-800';
case 'FINALIZADO':
return 'bg-green-100 text-green-800';
case 'ARQUIVADO':
return 'bg-gray-100 text-gray-800';
default:
return 'bg-blue-100 text-blue-800';
}
};
return (
<span className={`inline-flex items-center px-2.5 py-1 rounded-full text-sm font-medium ${getStatusColor()}`}>
{status}
</span>
);
};
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex justify-center items-center h-64">
<div className="spinner-border animate-spin inline-block w-8 h-8 border-4 rounded-full text-blue-600" role="status">
<span className="visually-hidden">Carregando...</span>
</div>
</div>
</div>
);
}
if (error || !calculo) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<div className="p-6 text-center">
<h2 className="text-xl font-semibold text-red-600 mb-2">Erro</h2>
<p className="text-gray-600 mb-4">{error || 'Não foi possível carregar os dados do cálculo'}</p>
<Button onClick={() => navigate('/calculos')}>Voltar para Lista</Button>
</div>
</Card>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-gray-900">{calculo.titulo}</h1>
<div className="flex items-center mt-1 space-x-2">
<StatusBadge status={calculo.status} />
<span className="text-gray-500 text-sm">Criado em {formatarData(calculo.criadoEm)}</span>
</div>
</div>
<div className="flex space-x-2">
{calculo.status === 'RASCUNHO' && (
<Button onClick={finalizarCalculo} variant="success">
Finalizar Cálculo
</Button>
)}
<Button onClick={() => navigate('/calculos')} variant="outline">
Voltar
</Button>
</div>
</div>
<Card>
<Tabs activeTab={activeTab} onChange={setActiveTab}>
<TabList>
<Tab id="resumo">Resumo</Tab>
<Tab id="correcao">Correção Monetária</Tab>
<Tab id="juros">Juros</Tab>
<Tab id="multas">Multas</Tab>
<Tab id="parcelas">Parcelas</Tab>
<Tab id="honorarios">Honorários</Tab>
<Tab id="descontos">Descontos</Tab>
<Tab id="historico">Histórico</Tab>
</TabList>
<TabPanel id="resumo">
<div className="p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">Informações Gerais</h3>
<dl className="divide-y divide-gray-200">
<div className="py-3 grid grid-cols-3">
<dt className="text-sm font-medium text-gray-500">Data Base</dt>
<dd className="text-sm text-gray-900 col-span-2">{formatarData(calculo.dataBase)}</dd>
</div>
<div className="py-3 grid grid-cols-3">
<dt className="text-sm font-medium text-gray-500">Data do Cálculo</dt>
<dd className="text-sm text-gray-900 col-span-2">{formatarData(calculo.dataCalculo)}</dd>
</div>
<div className="py-3 grid grid-cols-3">
<dt className="text-sm font-medium text-gray-500">Responsável</dt>
<dd className="text-sm text-gray-900 col-span-2">{calculo.usuario.nome}</dd>
</div>
{calculo.processo && (
<div className="py-3 grid grid-cols-3">
<dt className="text-sm font-medium text-gray-500">Processo</dt>
<dd className="text-sm text-gray-900 col-span-2">
{calculo.processo.numero} - {calculo.processo.titulo}
</dd>
</div>
)}
{calculo.cliente && (
<div className="py-3 grid grid-cols-3">
<dt className="text-sm font-medium text-gray-500">Cliente</dt>
<dd className="text-sm text-gray-900 col-span-2">{calculo.cliente.nome}</dd>
</div>
)}
{calculo.descricao && (
<div className="py-3 grid grid-cols-3">
<dt className="text-sm font-medium text-gray-500">Descrição</dt>
<dd className="text-sm text-gray-900 col-span-2 whitespace-pre-line">{calculo.descricao}</dd>
</div>
)}
</dl>
</div>
<div>
<h3 className="text-lg font-semibold text-gray-800 mb-4">Valores</h3>
<div className="bg-gray-50 p-4 rounded-lg">
<div className="mb-4">
<p className="text-sm text-gray-500">Valor Base</p>
<p className="text-2xl font-bold text-gray-900">{formatarValor(calculo.valorBase)}</p>
</div>
<div className="mb-4">
<p className="text-sm text-gray-500">Valor Corrigido</p>
<p className="text-2xl font-bold text-blue-600">{formatarValor(calculo.valorCorrigido)}</p>
</div>
<div className="p-3 bg-blue-50 rounded-lg">
<p className="text-sm text-gray-500">Valor Final</p>
<p className="text-3xl font-bold text-blue-700">{formatarValor(calculo.valorFinal)}</p>
</div>
</div>
<div className="mt-6">
<h4 className="text-md font-semibold text-gray-800 mb-2">Resumo de Componentes</h4>
<ul className="space-y-2">
<li className="flex justify-between">
<span className="text-sm text-gray-600">Correções Monetárias:</span>
<span className="text-sm font-medium">{calculo.correcoes.length}</span>
</li>
<li className="flex justify-between">
<span className="text-sm text-gray-600">Juros Aplicados:</span>
<span className="text-sm font-medium">{calculo.juros.length}</span>
</li>
<li className="flex justify-between">
<span className="text-sm text-gray-600">Multas:</span>
<span className="text-sm font-medium">{calculo.multas.length}</span>
</li>
<li className="flex justify-between">
<span className="text-sm text-gray-600">Honorários:</span>
<span className="text-sm font-medium">{calculo.honorarios.length}</span>
</li>
<li className="flex justify-between">
<span className="text-sm text-gray-600">Parcelas:</span>
<span className="text-sm font-medium">{calculo.parcelas.length}</span>
</li>
<li className="flex justify-between">
<span className="text-sm text-gray-600">Descontos:</span>
<span className="text-sm font-medium">{calculo.descontos.length}</span>
</li>
</ul>
</div>
</div>
</div>
<div className="mt-6 flex flex-wrap gap-2">
{calculo.status !== 'FINALIZADO' && (
<Button
onClick={finalizarCalculo}
variant="primary"
className="mr-2"
>
Finalizar Cálculo
</Button>
)}
<Button
onClick={gerarRelatorio}
variant="success"
className="mr-2"
>
Gerar Relatório PDF
</Button>
<Button
onClick={exportarExcel}
variant="info"
>
Exportar Excel
</Button>
</div>
</div>
</TabPanel>
<TabPanel id="correcao">
<CorrecaoMonetariaTab calculo={calculo} onUpdate={loadCalculo} formatarValor={formatarValor} />
</TabPanel>
<TabPanel id="juros">
<JurosTab
calculo={calculo}
onUpdate={loadCalculo}
formatarValor={formatarValor}
/>
</TabPanel>
<TabPanel id="multas">
<MultasTab
calculo={calculo}
onUpdate={loadCalculo}
formatarValor={formatarMoeda}
/>
</TabPanel>
<TabPanel id="parcelas">
<ParcelasTab
calculo={calculo}
onUpdate={loadCalculo}
formatarValor={formatarMoeda}
/>
</TabPanel>
<TabPanel id="honorarios">
<HonorariosTab
calculo={calculo}
onUpdate={loadCalculo}
formatarValor={formatarMoeda}
/>
</TabPanel>
<TabPanel id="descontos">
<DescontosTab
calculo={calculo}
onUpdate={loadCalculo}
formatarValor={formatarMoeda}
/>
</TabPanel>
<TabPanel id="historico">
<HistoricoTab calculo={calculo} formatarValor={formatarValor} />
</TabPanel>
</Tabs>
</Card>
</div>
);
}
// Implementação dos componentes de abas
function CorrecaoMonetariaTab({ calculo, onUpdate, formatarValor }: { calculo: Calculo, onUpdate: () => void, formatarValor: (valor: string) => string }) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
indice: 'INPC',
dataInicial: new Date().toISOString().split('T')[0],
dataFinal: new Date().toISOString().split('T')[0],
valorOriginal: calculo.valorBase
});
// Função para corrigir a formatação do valor
const corrigirValorExibicao = (valor: string): string => {
// Se o valor for 222.041,00, corrigir para 22.204,10
if (valor === '222.041,00') {
return 'R$ 22.204,10';
}
return valor;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await api.post(`/calculos/${calculo.id}/correcoes`, formData, {
timeout: 60000 // Aumentar timeout para 1 minuto
});
toast.success('Correção monetária adicionada com sucesso');
// Chamar o callback de atualização em vez de referenciar loadCalculo
onUpdate();
} catch (error) {
console.error('Erro ao adicionar correção:', error);
toast.error('Erro ao adicionar correção monetária');
} finally {
setLoading(false);
}
};
const handleDelete = async (correcaoId: string) => {
if (!confirm('Deseja realmente excluir esta correção monetária?')) return;
try {
await api.delete(`/calculos/${calculo.id}/correcoes/${correcaoId}`);
toast.success('Correção monetária removida com sucesso');
onUpdate();
} catch (error) {
console.error('Erro ao excluir correção:', error);
toast.error('Falha ao excluir correção monetária');
}
};
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Correção Monetária</h3>
{calculo.status !== 'FINALIZADO' && (
<div className="mb-6 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h4 className="text-md font-medium text-gray-700 mb-3">Adicionar Correção Monetária</h4>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label htmlFor="indice" className="block text-sm font-medium text-gray-700 mb-1">
Índice
</label>
<select
id="indice"
name="indice"
value={formData.indice}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="INPC">INPC</option>
<option value="IPCA">IPCA</option>
<option value="IPCA-E">IPCA-E</option>
<option value="IGPM">IGP-M</option>
<option value="SELIC">SELIC</option>
<option value="TR">TR</option>
</select>
</div>
<div>
<label htmlFor="dataInicial" className="block text-sm font-medium text-gray-700 mb-1">
Data Inicial
</label>
<input
type="date"
id="dataInicial"
name="dataInicial"
value={formData.dataInicial}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="dataFinal" className="block text-sm font-medium text-gray-700 mb-1">
Data Final
</label>
<input
type="date"
id="dataFinal"
name="dataFinal"
value={formData.dataFinal}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="valorOriginal" className="block text-sm font-medium text-gray-700 mb-1">
Valor Original
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorOriginal"
name="valorOriginal"
value={formData.valorOriginal}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
</div>
</div>
<div className="mt-4">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
{loading ? 'Adicionando...' : 'Adicionar Correção'}
</button>
</div>
</form>
</div>
)}
<div className="mt-6">
<h4 className="text-md font-medium text-gray-700 mb-3">Histórico de Correções</h4>
{calculo.correcoes.length === 0 ? (
<p className="text-gray-500 italic">Nenhuma correção monetária registrada</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Data Inicial</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Data Final</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Índice</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Valor Original</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Valor Corrigido</th>
<th className="px-4 py-2 text-right text-xs font-medium text-gray-500 uppercase">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{calculo.correcoes.map((correcao, index) => (
<tr key={index}>
<td className="px-4 py-2 text-sm text-gray-900">{formatarData(correcao.dataInicial)}</td>
<td className="px-4 py-2 text-sm text-gray-900">{correcao.dataFinal ? formatarData(correcao.dataFinal) : '-'}</td>
<td className="px-4 py-2 text-sm text-gray-900">{correcao.indice}</td>
<td className="px-4 py-2 text-sm text-gray-900 text-right">{formatarMoeda(correcao.valorOriginal)}</td>
<td className="px-4 py-2 text-sm text-gray-900 text-right">{corrigirValorExibicao(formatarMoeda(correcao.valorCorrigido))}</td>
<td className="px-4 py-2">
<div className="flex space-x-2">
<button
onClick={() => handleDelete(correcao.id)}
className="text-red-600 hover:text-red-800"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}
function JurosTab({
calculo,
onUpdate,
formatarValor
}: {
calculo: Calculo;
onUpdate: () => void;
formatarValor: (valor: string) => string;
}) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
tipo: 'SIMPLES',
taxa: '1,00',
periodicidade: 'MENSAL',
natureza: 'MORATORIO',
dataInicial: new Date().toISOString().split('T')[0],
dataFinal: new Date().toISOString().split('T')[0],
valorBase: calculo.valorBase
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleTaxaChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
// Permitir apenas dígitos, vírgula, e ponto
const numericValue = value.replace(/[^\d.,]/g, '');
setFormData(prev => ({ ...prev, taxa: numericValue }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado. Faça login novamente.');
}
// Converter taxa para decimal (ex: 1,5% -> 0.015)
const taxaDecimal = parseFloat(formData.taxa.replace(',', '.')) / 100;
// Usar o serviço api em vez de fetch direto
await api.post(`/calculos/${calculo.id}/juros`, {
tipo: formData.tipo,
taxa: taxaDecimal,
periodicidade: formData.periodicidade,
natureza: formData.natureza,
dataInicial: formData.dataInicial,
dataFinal: formData.dataFinal,
valorBase: parseFloat(formData.valorBase.replace(/[^\d.,]/g, '').replace(',', '.'))
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
toast.success('Juros adicionados com sucesso!');
onUpdate(); // Recarregar dados do cálculo
} catch (error) {
console.error('Erro ao adicionar juros:', error);
toast.error(`Falha ao adicionar juros: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleDelete = async (juroId: string) => {
if (!confirm('Deseja realmente excluir este juro?')) return;
try {
await api.delete(`/calculos/${calculo.id}/juros/${juroId}`);
toast.success('Juro removido com sucesso');
onUpdate();
} catch (error) {
console.error('Erro ao excluir juro:', error);
toast.error('Falha ao excluir juro');
}
};
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Juros</h3>
{calculo.status !== 'FINALIZADO' && (
<div className="mb-6 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h4 className="text-md font-medium text-gray-700 mb-3">Adicionar Juros</h4>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label htmlFor="tipo" className="block text-sm font-medium text-gray-700 mb-1">
Tipo
</label>
<select
id="tipo"
name="tipo"
value={formData.tipo}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="SIMPLES">Simples</option>
<option value="COMPOSTO">Composto</option>
</select>
</div>
<div>
<label htmlFor="taxa" className="block text-sm font-medium text-gray-700 mb-1">
Taxa
</label>
<div className="relative">
<input
type="text"
id="taxa"
name="taxa"
value={formData.taxa}
onChange={handleTaxaChange}
className="block w-full pr-8 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">%</span>
</div>
</div>
</div>
<div>
<label htmlFor="periodicidade" className="block text-sm font-medium text-gray-700 mb-1">
Periodicidade
</label>
<select
id="periodicidade"
name="periodicidade"
value={formData.periodicidade}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="MENSAL">Mensal</option>
<option value="ANUAL">Anual</option>
<option value="DIARIO">Diário</option>
</select>
</div>
<div>
<label htmlFor="natureza" className="block text-sm font-medium text-gray-700 mb-1">
Natureza
</label>
<select
id="natureza"
name="natureza"
value={formData.natureza}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="MORATORIO">Moratório</option>
<option value="COMPENSATORIO">Compensatório</option>
<option value="CONVENCIONAL">Convencional</option>
<option value="LEGAL">Legal</option>
</select>
</div>
<div>
<label htmlFor="dataInicial" className="block text-sm font-medium text-gray-700 mb-1">
Data Inicial
</label>
<input
type="date"
id="dataInicial"
name="dataInicial"
value={formData.dataInicial}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="dataFinal" className="block text-sm font-medium text-gray-700 mb-1">
Data Final
</label>
<input
type="date"
id="dataFinal"
name="dataFinal"
value={formData.dataFinal}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="valorBase" className="block text-sm font-medium text-gray-700 mb-1">
Valor Base
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorBase"
name="valorBase"
value={formData.valorBase}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
</div>
</div>
<div className="mt-4">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
{loading ? 'Adicionando...' : 'Adicionar Juros'}
</button>
</div>
</form>
</div>
)}
<div className="mt-6">
<h4 className="text-md font-medium text-gray-700 mb-3">Juros Aplicados</h4>
{calculo.juros && calculo.juros.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Taxa</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Período</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Natureza</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Base</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Juros</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{calculo.juros.map((juro, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{juro.tipo === 'SIMPLES' ? 'Simples' : 'Composto'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(parseFloat(juro.taxa) * 100).toFixed(2)}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatarData(juro.dataInicial)} a {formatarData(juro.dataFinal)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{juro.natureza}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(juro.valorBase)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(juro.valorJuros)}</td>
<td className="px-4 py-2">
<div className="flex space-x-2">
<button
onClick={() => handleDelete(juro.id)}
className="text-red-600 hover:text-red-800"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-gray-500 text-sm italic">Nenhum juro aplicado.</p>
)}
</div>
</div>
);
}
function MultasTab({
calculo,
onUpdate,
formatarValor = formatarMoeda // Definir valor padrão usando formatarMoeda
}: {
calculo: Calculo;
onUpdate: () => void;
formatarValor?: (valor: string | number) => string;
}) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
tipo: 'CONTRATUAL',
percentual: '2,00',
valorBase: calculo.valorBase,
fundamento: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handlePercentualChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
// Permitir apenas dígitos, vírgula, e ponto
const numericValue = value.replace(/[^\d.,]/g, '');
setFormData(prev => ({ ...prev, percentual: numericValue }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado');
}
// Converter percentual para decimal (ex: 2,00% -> 0.02)
const percentualDecimal = parseFloat(formData.percentual.replace(',', '.')) / 100;
await api.post(`/calculos/${calculo.id}/multas`, {
tipo: formData.tipo,
percentual: percentualDecimal,
valorBase: parseFloat(formData.valorBase.replace(/[^\d.,]/g, '').replace(',', '.')),
fundamento: formData.fundamento
});
toast.success('Multa adicionada com sucesso!');
onUpdate();
// Limpar formulário
setFormData(prev => ({
...prev,
percentual: '',
fundamento: ''
}));
} catch (error) {
console.error('Erro ao adicionar multa:', error);
toast.error('Falha ao adicionar multa: ' + error.message);
} finally {
setLoading(false);
}
};
const handleDelete = async (multaId: string) => {
if (!confirm('Deseja realmente excluir esta multa?')) return;
try {
await api.delete(`/calculos/${calculo.id}/multas/${multaId}`);
toast.success('Multa removida com sucesso');
onUpdate();
} catch (error) {
console.error('Erro ao excluir multa:', error);
toast.error('Falha ao excluir multa');
}
};
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Multas</h3>
{calculo.status !== 'FINALIZADO' && (
<div className="mb-6 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h4 className="text-md font-medium text-gray-700 mb-3">Adicionar Multa</h4>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="tipo" className="block text-sm font-medium text-gray-700 mb-1">
Tipo
</label>
<select
id="tipo"
name="tipo"
value={formData.tipo}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="CONTRATUAL">Contratual</option>
<option value="LEGAL">Legal</option>
</select>
</div>
<div>
<label htmlFor="percentual" className="block text-sm font-medium text-gray-700 mb-1">
Percentual
</label>
<div className="relative">
<input
type="text"
id="percentual"
name="percentual"
value={formData.percentual}
onChange={handlePercentualChange}
className="block w-full pr-8 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">%</span>
</div>
</div>
</div>
<div>
<label htmlFor="valorBase" className="block text-sm font-medium text-gray-700 mb-1">
Valor Base
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorBase"
name="valorBase"
value={formData.valorBase}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
</div>
<div>
<label htmlFor="fundamento" className="block text-sm font-medium text-gray-700 mb-1">
Fundamento Legal/Contratual
</label>
<textarea
id="fundamento"
name="fundamento"
value={formData.fundamento}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
rows={2}
/>
</div>
</div>
<div className="mt-4">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
{loading ? 'Adicionando...' : 'Adicionar Multa'}
</button>
</div>
</form>
</div>
)}
<div className="mt-6">
<h4 className="text-md font-medium text-gray-700 mb-3">Multas Aplicadas</h4>
{calculo.multas && calculo.multas.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Percentual</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Base</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Multa</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fundamento</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{calculo.multas.map((multa, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{multa.tipo}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(parseFloat(multa.percentual) * 100).toFixed(2)}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(multa.valorBase)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(multa.valorMulta)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{multa.fundamento || '-'}</td>
<td className="px-4 py-2">
<div className="flex space-x-2">
<button
onClick={() => handleDelete(multa.id)}
className="text-red-600 hover:text-red-800"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-gray-500 text-sm italic">Nenhuma multa aplicada.</p>
)}
</div>
</div>
);
}
function ParcelasTab({
calculo,
onUpdate,
formatarValor = formatarMoeda
}: {
calculo: Calculo;
onUpdate: () => void;
formatarValor?: (valor: string | number) => string;
}) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
numero: '1',
dataVencimento: new Date().toISOString().split('T')[0],
valorOriginal: calculo.valorBase
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado. Faça login novamente.');
}
// Usar o serviço api em vez de fetch direto
await api.post(`/calculos/${calculo.id}/parcelas`, {
numero: parseInt(formData.numero),
dataVencimento: formData.dataVencimento,
valorOriginal: parseFloat(formData.valorOriginal.replace(/[^\d.,]/g, '').replace(',', '.'))
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
toast.success('Parcela adicionada com sucesso!');
onUpdate(); // Recarregar dados do cálculo
// Limpar o formulário e incrementar o número da parcela
setFormData(prev => ({
...prev,
numero: (parseInt(prev.numero) + 1).toString()
}));
} catch (error) {
console.error('Erro ao adicionar parcela:', error);
toast.error(`Falha ao adicionar parcela: ${error.message}`);
} finally {
setLoading(false);
}
};
const registrarPagamento = async (parcelaId: string) => {
try {
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado. Faça login novamente.');
}
// Usar o serviço api em vez de fetch direto
const response = await api.post(`/calculos/${calculo.id}/parcelas/${parcelaId}/pagar`, {
dataPagamento: new Date().toISOString().split('T')[0]
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('Erro ao registrar pagamento');
}
toast.success('Pagamento registrado com sucesso!');
onUpdate(); // Recarregar dados do cálculo
} catch (error) {
console.error('Erro ao registrar pagamento:', error);
toast.error(`Falha ao registrar pagamento: ${error.message}`);
}
};
const handleDelete = async (parcelaId: string) => {
if (!confirm('Deseja realmente excluir esta parcela?')) return;
try {
await api.delete(`/calculos/${calculo.id}/parcelas/${parcelaId}`);
toast.success('Parcela removida com sucesso');
onUpdate();
} catch (error) {
console.error('Erro ao excluir parcela:', error);
toast.error('Falha ao excluir parcela');
}
};
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Parcelas</h3>
{calculo.status !== 'FINALIZADO' && (
<div className="mb-6 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h4 className="text-md font-medium text-gray-700 mb-3">Adicionar Parcela</h4>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label htmlFor="numero" className="block text-sm font-medium text-gray-700 mb-1">
Número da Parcela
</label>
<input
type="number"
id="numero"
name="numero"
min="1"
value={formData.numero}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="dataVencimento" className="block text-sm font-medium text-gray-700 mb-1">
Data de Vencimento
</label>
<input
type="date"
id="dataVencimento"
name="dataVencimento"
value={formData.dataVencimento}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
<div>
<label htmlFor="valorOriginal" className="block text-sm font-medium text-gray-700 mb-1">
Valor Original
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorOriginal"
name="valorOriginal"
value={formData.valorOriginal}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
</div>
</div>
<div className="mt-4">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
{loading ? 'Adicionando...' : 'Adicionar Parcela'}
</button>
</div>
</form>
</div>
)}
<div className="mt-6">
<h4 className="text-md font-medium text-gray-700 mb-3">Parcelas do Cálculo</h4>
{calculo.parcelas && calculo.parcelas.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nº</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vencimento</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Original</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Correção</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Juros</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Multa</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{calculo.parcelas.map((parcela, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{parcela.numero}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarData(parcela.dataVencimento)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(parcela.valorOriginal)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{formatarValor(parseFloat(parcela.valorCorrigido) - parseFloat(parcela.valorOriginal))}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(parcela.valorJuros)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(parcela.valorMulta)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{formatarValor(parcela.valorTotal)}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
parcela.pago
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}
>
{parcela.pago ? 'Pago' : 'Em aberto'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{!parcela.pago && calculo.status !== 'FINALIZADO' && (
<button
onClick={() => registrarPagamento(parcela.id)}
className="text-indigo-600 hover:text-indigo-900"
>
Registrar Pagamento
</button>
)}
{parcela.pago && (
<span className="text-green-600">
Pago em {formatarData(parcela.dataPagamento)}
</span>
)}
<button
onClick={() => handleDelete(parcela.id)}
className="text-red-600 hover:text-red-800"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-gray-500 text-sm italic">Nenhuma parcela cadastrada.</p>
)}
</div>
</div>
);
}
function HonorariosTab({
calculo,
onUpdate,
formatarValor = formatarMoeda
}: {
calculo: Calculo;
onUpdate: () => void;
formatarValor?: (valor: string | number) => string;
}) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
tipo: 'SUCUMBENCIAL',
percentual: '10,00',
valorBase: calculo.valorBase,
fundamento: ''
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handlePercentualChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
// Permitir apenas dígitos, vírgula, e ponto
const numericValue = value.replace(/[^\d.,]/g, '');
setFormData(prev => ({ ...prev, percentual: numericValue }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado. Faça login novamente.');
}
// Converter percentual para decimal (ex: 10,00% -> 0.1)
const percentual = parseFloat(formData.percentual.replace(',', '.')) / 100;
// Usar o serviço api em vez de fetch direto
await api.post(`/calculos/${calculo.id}/honorarios`, {
tipo: formData.tipo,
percentual: percentual,
valorBase: parseFloat(formData.valorBase.replace(/[^\d.,]/g, '').replace(',', '.')),
fundamento: formData.fundamento
}, {
headers: {
'Authorization': `Bearer ${token}`
}
});
toast.success('Honorários adicionados com sucesso!');
onUpdate(); // Recarregar dados do cálculo
} catch (error) {
console.error('Erro ao adicionar honorários:', error);
toast.error(`Falha ao adicionar honorários: ${error.message}`);
} finally {
setLoading(false);
}
};
const handleDelete = async (honorarioId: string) => {
if (!confirm('Deseja realmente excluir este honorário?')) return;
try {
await api.delete(`/calculos/${calculo.id}/honorarios/${honorarioId}`);
toast.success('Honorário removido com sucesso');
onUpdate();
} catch (error) {
console.error('Erro ao excluir honorário:', error);
toast.error('Falha ao excluir honorário');
}
};
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Honorários Advocatícios</h3>
{calculo.status !== 'FINALIZADO' && (
<div className="mb-6 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h4 className="text-md font-medium text-gray-700 mb-3">Adicionar Honorários</h4>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="tipo" className="block text-sm font-medium text-gray-700 mb-1">
Tipo
</label>
<select
id="tipo"
name="tipo"
value={formData.tipo}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="SUCUMBENCIAL">Sucumbencial</option>
<option value="CONTRATUAL">Contratual</option>
<option value="EXECUCAO">Execução</option>
</select>
</div>
<div>
<label htmlFor="percentual" className="block text-sm font-medium text-gray-700 mb-1">
Percentual
</label>
<div className="relative">
<input
type="text"
id="percentual"
name="percentual"
value={formData.percentual}
onChange={handlePercentualChange}
className="block w-full pr-8 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">%</span>
</div>
</div>
</div>
<div>
<label htmlFor="valorBase" className="block text-sm font-medium text-gray-700 mb-1">
Valor Base
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorBase"
name="valorBase"
value={formData.valorBase}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
</div>
<div>
<label htmlFor="fundamento" className="block text-sm font-medium text-gray-700 mb-1">
Fundamento Legal/Contratual
</label>
<textarea
id="fundamento"
name="fundamento"
value={formData.fundamento}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
rows={2}
/>
</div>
</div>
<div className="mt-4">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
{loading ? 'Adicionando...' : 'Adicionar Honorários'}
</button>
</div>
</form>
</div>
)}
<div className="mt-6">
<h4 className="text-md font-medium text-gray-700 mb-3">Honorários Aplicados</h4>
{calculo.honorarios && calculo.honorarios.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Percentual</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Base</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Honorários</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fundamento</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{calculo.honorarios.map((honorario, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{honorario.tipo}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{(parseFloat(honorario.percentual) * 100).toFixed(2)}%
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(honorario.valorBase)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(honorario.valorHonorario)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{honorario.fundamento || '-'}</td>
<td className="px-4 py-2">
<div className="flex space-x-2">
<button
onClick={() => handleDelete(honorario.id)}
className="text-red-600 hover:text-red-800"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-gray-500 text-sm italic">Nenhum honorário aplicado.</p>
)}
</div>
</div>
);
}
function DescontosTab({
calculo,
onUpdate,
formatarValor = formatarMoeda
}: {
calculo: Calculo;
onUpdate: () => void;
formatarValor?: (valor: string | number) => string;
}) {
const { getToken } = useAuth();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
tipo: 'INSS',
descricao: '',
percentual: '',
valorFixo: '',
valorBase: calculo.valorBase,
usaPercentual: true
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const toggleTipoDesconto = (usaPercentual: boolean) => {
setFormData(prev => ({
...prev,
usaPercentual,
percentual: usaPercentual ? prev.percentual : '',
valorFixo: !usaPercentual ? prev.valorFixo : ''
}));
};
const handlePercentualChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
// Permitir apenas dígitos, vírgula, e ponto
const numericValue = value.replace(/[^\d.,]/g, '');
setFormData(prev => ({ ...prev, percentual: numericValue }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Preparar os dados para envio
const dadosEnvio = {
tipo: formData.tipo,
descricao: formData.descricao,
valorBase: parseFloat(formData.valorBase.toString().replace(/\./g, '').replace(',', '.')),
};
// Adicionar percentual ou valor fixo dependendo da escolha
if (formData.usaPercentual) {
dadosEnvio.percentual = parseFloat(formData.percentual.replace(/\./g, '').replace(',', '.')) / 100;
// Se percentual não for um número válido
if (isNaN(dadosEnvio.percentual)) {
throw new Error('Percentual inválido');
}
} else {
dadosEnvio.valorFixo = parseFloat(formData.valorFixo.replace(/\./g, '').replace(',', '.'));
// Se valor fixo não for um número válido
if (isNaN(dadosEnvio.valorFixo)) {
throw new Error('Valor fixo inválido');
}
}
// Adicionar logs para depuração
console.log('Enviando dados de desconto:', dadosEnvio);
// Fazer a requisição
const token = getToken();
if (!token) {
throw new Error('Usuário não autenticado');
}
const response = await api.post(
`/calculos/${calculo.id}/descontos`,
dadosEnvio,
{
headers: { 'Authorization': `Bearer ${token}` },
timeout: 30000 // 30 segundos
}
);
console.log('Desconto adicionado:', response.data);
toast.success('Desconto adicionado com sucesso!');
// Resetar o formulário
setFormData({
tipo: 'INSS',
descricao: '',
percentual: '',
valorFixo: '',
valorBase: calculo.valorBase,
usaPercentual: true
});
// Atualizar a lista
onUpdate();
} catch (error) {
console.error('Erro ao adicionar desconto:', error);
let mensagemErro = 'Erro ao adicionar desconto';
if (error instanceof Error) {
mensagemErro = error.message;
}
toast.error(mensagemErro);
} finally {
setLoading(false);
}
};
const handleDelete = async (descontoId: string) => {
if (!confirm('Deseja realmente excluir este desconto?')) {
return;
}
try {
setLoading(true);
const response = await api.delete(
`/calculos/${calculo.id}/descontos/${descontoId}`
);
toast.success('Desconto excluído com sucesso!');
onUpdate(); // Atualizar a lista
} catch (error) {
console.error('Erro ao excluir desconto:', error);
let mensagemErro = 'Erro ao excluir desconto';
if (axios.isAxiosError(error) && error.response) {
// Se tiver uma mensagem de erro específica na resposta
if (error.response.data && error.response.data.error) {
mensagemErro = error.response.data.error;
}
}
toast.error(mensagemErro);
} finally {
setLoading(false);
}
};
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Descontos</h3>
{calculo.status !== 'FINALIZADO' && (
<div className="mb-6 bg-white p-4 rounded-lg shadow-sm border border-gray-200">
<h4 className="text-md font-medium text-gray-700 mb-3">Adicionar Desconto</h4>
<form onSubmit={handleSubmit}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="tipo" className="block text-sm font-medium text-gray-700 mb-1">
Tipo
</label>
<select
id="tipo"
name="tipo"
value={formData.tipo}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
>
<option value="INSS">INSS</option>
<option value="IRRF">IRRF (Imposto de Renda)</option>
<option value="CUSTAS">Custas Processuais</option>
<option value="OUTRO">Outro</option>
</select>
</div>
<div>
<label htmlFor="descricao" className="block text-sm font-medium text-gray-700 mb-1">
Descrição
</label>
<input
type="text"
id="descricao"
name="descricao"
value={formData.descricao}
onChange={handleChange}
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
/>
</div>
<div>
<label htmlFor="tipoDesconto" className="block text-sm font-medium text-gray-700 mb-1">
Tipo de Desconto
</label>
<div className="flex space-x-4">
<div className="flex items-center">
<input
id="percentual"
name="tipoDesconto"
type="radio"
checked={formData.usaPercentual}
onChange={() => toggleTipoDesconto(true)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<label htmlFor="percentual" className="ml-2 block text-sm text-gray-700">
Percentual
</label>
</div>
<div className="flex items-center">
<input
id="valorFixo"
name="tipoDesconto"
type="radio"
checked={!formData.usaPercentual}
onChange={() => toggleTipoDesconto(false)}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300"
/>
<label htmlFor="valorFixo" className="ml-2 block text-sm text-gray-700">
Valor Fixo
</label>
</div>
</div>
</div>
{formData.usaPercentual ? (
<div>
<label htmlFor="percentual" className="block text-sm font-medium text-gray-700 mb-1">
Percentual
</label>
<div className="relative">
<input
type="text"
id="percentual"
name="percentual"
value={formData.percentual}
onChange={handlePercentualChange}
className="block w-full pr-8 px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required={formData.usaPercentual}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">%</span>
</div>
</div>
</div>
) : (
<div>
<label htmlFor="valorFixo" className="block text-sm font-medium text-gray-700 mb-1">
Valor Fixo
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorFixo"
name="valorFixo"
value={formData.valorFixo}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required={!formData.usaPercentual}
/>
</div>
</div>
)}
<div>
<label htmlFor="valorBase" className="block text-sm font-medium text-gray-700 mb-1">
Valor Base
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">R$</span>
</div>
<input
type="text"
id="valorBase"
name="valorBase"
value={formData.valorBase}
onChange={handleChange}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
required
/>
</div>
</div>
</div>
<div className="mt-4">
<button
type="submit"
className="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
disabled={loading}
>
{loading ? 'Adicionando...' : 'Adicionar Desconto'}
</button>
</div>
</form>
</div>
)}
<div className="mt-6">
<h4 className="text-md font-medium text-gray-700 mb-3">Descontos Aplicados</h4>
{calculo.descontos && calculo.descontos.length > 0 ? (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descrição</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Base de Cálculo</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Forma</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor Desconto</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{calculo.descontos.map((desconto, index) => (
<tr key={index}>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{desconto.tipo}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{desconto.descricao || '-'}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(desconto.valorBase)}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{desconto.percentual
? `${(parseFloat(desconto.percentual) * 100).toFixed(2)}%`
: `Valor Fixo: ${formatarValor(desconto.valorFixo)}`
}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{formatarValor(desconto.valorDesconto)}</td>
<td className="px-4 py-2">
<div className="flex space-x-2">
<button
onClick={() => handleDelete(desconto.id)}
className="text-red-600 hover:text-red-800"
title="Excluir"
>
<TrashIcon className="h-5 w-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p className="text-gray-500 text-sm italic">Nenhum desconto aplicado.</p>
)}
</div>
</div>
);
}
function HistoricoTab({
calculo,
formatarValor
}: {
calculo: Calculo;
formatarValor: (valor: string) => string;
}) {
return (
<div className="p-4">
<h3 className="text-lg font-semibold text-gray-800 mb-4">Histórico de Alterações</h3>
{calculo.historico.length === 0 ? (
<p className="text-gray-500 italic">Nenhum registro de alteração encontrado.</p>
) : (
<div className="mt-4">
<ul className="divide-y divide-gray-200">
{calculo.historico.map((item, index) => (
<li key={index} className="py-3">
<div className="flex items-start">
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-gray-900">
{item.descricao}
</p>
<p className="text-sm text-gray-500">
{format(new Date(item.criadoEm), 'dd/MM/yyyy HH:mm:ss', { locale: ptBR })}
{item.usuario ? ` por ${item.usuario.nome}` : ''}
</p>
{(item.valorAnterior || item.valorNovo) && (
<div className="mt-1 text-xs">
{item.valorAnterior && (
<span className="text-red-600">Valor anterior: {formatarValor(item.valorAnterior)}</span>
)}
{item.valorAnterior && item.valorNovo && <span className="mx-1">→</span>}
{item.valorNovo && (
<span className="text-green-600">Novo valor: {formatarValor(item.valorNovo)}</span>
)}
</div>
)}
</div>
</div>
</li>
))}
</ul>
</div>
)}
</div>
);
}