2010 lines
80 KiB
TypeScript
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>
|
|
);
|
|
}
|