despesas e receitas
All checks were successful
Frontend Build and Deploy / build (push) Successful in 22s

This commit is contained in:
José Henrique 2025-06-07 15:18:29 -03:00
parent 475979a09a
commit 83ff2131f7
7 changed files with 809 additions and 3 deletions

View File

@ -73,4 +73,54 @@ export interface Partido {
nome: string;
sigla: string;
numero: number;
}
}
export interface CandidateExpenses {
despesas: Expense[];
}
export interface Expense {
idDespesa: string;
idCandidato: string;
ano: number;
turno: number;
sqCandidato: string;
sgPartido: string;
tipoFornecedor: string;
cpfFornecedor: string;
cnpjFornecedor: string;
nomeFornecedor: string;
nomeFornecedorRFB: string;
municipioFornecedor: string;
tipoDocumento: string;
dataDespesa: string; // ISO date
descricao: string;
origemDespesa: string;
valor: number;
}
export interface CandidateIncome {
receitas: Income[];
}
export interface Income {
idReceita: string;
idCandidato: string;
ano: number;
turno: number;
sqCandidato: string;
sgPartido: string;
fonteReceita: string;
origemReceita: string;
naturezaReceita: string;
especieReceita: string;
cpfDoador: string;
cnpjDoador: string;
nomeDoador: string;
nomeDoadorRFB: string;
municipioDoador: string;
dataReceita: string; // ISO date
descricao: string;
valor: number;
}

View File

@ -10,6 +10,12 @@ export type {
CandidateRedesSociais,
RedeSocial,
PlatformStats,
CpfRevealResult,
Partido,
CandidateExpenses,
Expense,
CandidateIncome,
Income,
} from './apiModels';
// Export base API classes for custom implementations

View File

@ -1,6 +1,6 @@
import { BaseApiClient } from './base';
import { API_CONFIG } from '../config/api';
import type { CandidateAssets, CandidateDetails, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, PlatformStats } from './apiModels';
import type { CandidateAssets, CandidateDetails, CandidateExpenses, CandidateIncome, CandidateRedesSociais, CandidateSearchResult, CpfRevealResult, PlatformStats } from './apiModels';
/**
* OpenCand API client for interacting with the OpenCand platform
@ -53,6 +53,20 @@ export class OpenCandApi extends BaseApiClient {
async getCandidateCpf(id: string): Promise<CpfRevealResult> {
return this.get<CpfRevealResult>(`/v1/candidato/${id}/reveal-cpf`);
}
/**
* Get the expenses for a specific candidate by ID and year
*/
async getCandidateDepesas(id: string): Promise<CandidateExpenses> {
return this.get<CandidateExpenses>(`/v1/candidato/${id}/despesas`);
}
/**
* Get the expenses for a specific candidate by ID and year
*/
async getCandidateReceitas(id: string): Promise<CandidateIncome> {
return this.get<CandidateIncome>(`/v1/candidato/${id}/receitas`);
}
}
// Create a default instance for easy usage with proper configuration

View File

@ -1,11 +1,12 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
import { openCandApi, type CandidateDetails, type CandidateAssets, type CandidateRedesSociais, ApiError } from '../../api';
import { openCandApi, type CandidateDetails, type CandidateAssets, type CandidateRedesSociais, type CandidateExpenses, type CandidateIncome, ApiError } from '../../api';
import ElectionsComponent from './ElectionsComponent';
import AssetsComponent from './AssetsComponent';
import BasicCandidateInfoComponent from './BasicCandidateInfoComponent';
import SocialMediaComponent from './SocialMediaComponent';
import IncomeExpenseComponent from './IncomeExpenseComponent';
const CandidatePage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@ -13,9 +14,13 @@ const CandidatePage: React.FC = () => {
const [candidateDetails, setCandidateDetails] = useState<CandidateDetails | null>(null);
const [candidateAssets, setCandidateAssets] = useState<CandidateAssets | null>(null);
const [candidateRedesSociais, setCandidateRedesSociais] = useState<CandidateRedesSociais | null>(null);
const [candidateExpenses, setCandidateExpenses] = useState<CandidateExpenses | null>(null);
const [candidateIncome, setCandidateIncome] = useState<CandidateIncome | null>(null);
const [isLoadingDetails, setIsLoadingDetails] = useState(true);
const [isLoadingAssets, setIsLoadingAssets] = useState(true);
const [isLoadingRedesSociais, setIsLoadingRedesSociais] = useState(true);
const [isLoadingExpenses, setIsLoadingExpenses] = useState(true);
const [isLoadingIncome, setIsLoadingIncome] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
@ -70,9 +75,39 @@ const CandidatePage: React.FC = () => {
}
};
// Fetch candidate expenses
const fetchCandidateExpenses = async () => {
try {
setIsLoadingExpenses(true);
const expenses = await openCandApi.getCandidateDepesas(id);
setCandidateExpenses(expenses);
} catch (err) {
console.error('Error fetching candidate expenses:', err);
// Expenses might not be available for all candidates, so we don't set error here
} finally {
setIsLoadingExpenses(false);
}
};
// Fetch candidate income
const fetchCandidateIncome = async () => {
try {
setIsLoadingIncome(true);
const income = await openCandApi.getCandidateReceitas(id);
setCandidateIncome(income);
} catch (err) {
console.error('Error fetching candidate income:', err);
// Income might not be available for all candidates, so we don't set error here
} finally {
setIsLoadingIncome(false);
}
};
fetchCandidateDetails();
fetchCandidateAssets();
fetchCandidateRedesSociais();
fetchCandidateExpenses();
fetchCandidateIncome();
}, [id, navigate]);
if (error) {
@ -138,6 +173,14 @@ const CandidatePage: React.FC = () => {
assets={candidateAssets?.bens || null}
isLoading={isLoadingAssets}
/>
{/* Income and Expenses Panel */}
<IncomeExpenseComponent
expenses={candidateExpenses}
income={candidateIncome}
isLoadingExpenses={isLoadingExpenses}
isLoadingIncome={isLoadingIncome}
/>
</div>
</div>
</main>

View File

@ -0,0 +1,321 @@
import React, { useState, useMemo } from 'react';
import {
ChevronDownIcon,
ChevronUpIcon,
ArrowTrendingDownIcon,
CalendarIcon,
UserIcon,
BuildingOfficeIcon
} from '@heroicons/react/24/outline';
import { type CandidateExpenses, type Expense } from '../../../api';
interface ExpenseSectionProps {
expenses: CandidateExpenses | null;
isLoadingExpenses: boolean;
}
const ExpenseSection: React.FC<ExpenseSectionProps> = ({
expenses,
isLoadingExpenses
}) => {
const [expandedYears, setExpandedYears] = useState<Set<number>>(new Set());
const [visibleExpenseItems, setVisibleExpenseItems] = useState<Record<number, number>>({});
const [expandedExpenseItems, setExpandedExpenseItems] = useState<Set<string>>(new Set());
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(value);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('pt-BR');
};
const groupExpensesByYear = useMemo(() => {
const despesas = expenses?.despesas || [];
if (!despesas.length) return {};
return despesas.reduce((acc: Record<number, Expense[]>, item: Expense) => {
const year = item.ano;
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(item);
return acc;
}, {} as Record<number, Expense[]>);
}, [expenses]);
// Sort items by value (descending) for each year
const sortedExpensesByYear = useMemo(() => {
const sorted: Record<number, Expense[]> = {};
Object.keys(groupExpensesByYear).forEach(year => {
const yearNum = Number(year);
sorted[yearNum] = [...groupExpensesByYear[yearNum]].sort((a, b) => b.valor - a.valor);
});
return sorted;
}, [groupExpensesByYear]);
const sortedExpenseYears = useMemo(() => {
return Object.keys(sortedExpensesByYear).map(Number).sort((a, b) => b - a);
}, [sortedExpensesByYear]);
const getTotalExpenses = () => {
const despesas = expenses?.despesas || [];
return despesas.reduce((total: number, item: Expense) => total + item.valor, 0);
};
const getTotalForExpenseYear = (yearExpenses: Expense[]) => {
return yearExpenses.reduce((total, item) => total + item.valor, 0);
};
const getVisibleExpenseItemsForYear = (year: number) => {
return visibleExpenseItems[year] || 50;
};
const loadMoreExpenseItems = (year: number) => {
setVisibleExpenseItems(prev => ({
...prev,
[year]: (prev[year] || 50) + 50
}));
};
const toggleYear = (year: number) => {
setExpandedYears(prev => {
const newSet = new Set(prev);
if (newSet.has(year)) {
newSet.delete(year);
} else {
newSet.add(year);
}
return newSet;
});
};
const toggleExpenseItem = (itemId: string) => {
setExpandedExpenseItems(prev => {
const newSet = new Set(prev);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
newSet.add(itemId);
}
return newSet;
});
};
const despesas = expenses?.despesas || [];
const isEmpty = despesas.length === 0;
return (
<div>
<div className="flex items-center mb-4">
<ArrowTrendingDownIcon className="h-6 w-6 text-red-600 mr-2" />
<h3 className="text-xl font-bold text-gray-900">Despesas</h3>
</div>
{isLoadingExpenses ? (
<div className="flex items-center justify-center h-24">
<div className="animate-spin rounded-full h-6 w-6 border-4 border-red-600 border-t-transparent"></div>
</div>
) : !isEmpty ? (
<>
{/* Summary Card */}
<div className="bg-gradient-to-r from-red-50 to-orange-50 p-4 rounded-lg mb-4 border border-red-100">
<div className="flex items-center justify-between">
<span className="text-lg font-semibold text-gray-900">Total de Despesas</span>
<span className="text-xl font-bold text-red-600">
{formatCurrency(getTotalExpenses())}
</span>
</div>
</div>
<div className="space-y-3">
{sortedExpenseYears.map((year) => {
const yearExpenses = sortedExpensesByYear[year];
const isExpanded = expandedYears.has(year + 10000); // Add offset to avoid conflicts with income years
const visibleCount = getVisibleExpenseItemsForYear(year);
const hasMore = yearExpenses.length > visibleCount;
const visibleItems = yearExpenses.slice(0, visibleCount);
return (
<div key={`expense-${year}`} className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => toggleYear(year + 10000)}
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 flex items-center justify-between transition-colors"
>
<div className="flex items-center">
<CalendarIcon className="h-5 w-5 text-gray-600 mr-2" />
<span className="font-semibold text-gray-900">{year}</span>
<span className="ml-2 text-sm text-gray-600">
({yearExpenses.length} {yearExpenses.length === 1 ? 'item' : 'itens'})
</span>
</div>
<div className="flex items-center">
<span className="font-bold text-red-600 mr-2">
{formatCurrency(getTotalForExpenseYear(yearExpenses))}
</span>
{isExpanded ? (
<ChevronUpIcon className="h-5 w-5 text-gray-600" />
) : (
<ChevronDownIcon className="h-5 w-5 text-gray-600" />
)}
</div>
</button>
{isExpanded && (
<div className="bg-white">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-4 py-3 text-left font-medium text-gray-900">Data</th>
<th className="px-4 py-3 text-left font-medium text-gray-900">Fornecedor</th>
<th className="px-4 py-3 text-left font-medium text-gray-900">Descrição</th>
<th className="px-4 py-3 text-right font-medium text-gray-900">Valor</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{visibleItems.map((item, index) => {
const itemId = `expense-${item.ano}-${index}`;
const isItemExpanded = expandedExpenseItems.has(itemId);
return (
<React.Fragment key={itemId}>
<tr
className="hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => toggleExpenseItem(itemId)}
>
<td className="px-4 py-3 text-gray-900">
{formatDate(item.dataDespesa)}
</td>
<td className="px-4 py-3">
<div className="flex items-center">
{item.cpfFornecedor ? (
<UserIcon className="h-4 w-4 text-gray-400 mr-2" />
) : (
<BuildingOfficeIcon className="h-4 w-4 text-gray-400 mr-2" />
)}
<span className="text-gray-900 font-medium">
{item.nomeFornecedor || item.nomeFornecedorRFB || 'Fornecedor não informado'}
</span>
</div>
</td>
<td className="px-4 py-3 text-gray-600 max-w-xs truncate">
{item.descricao || 'Descrição não informada'}
</td>
<td className="px-4 py-3 text-right font-semibold">
<div className="flex items-center justify-end">
<span className="text-red-600 mr-2">
{formatCurrency(item.valor)}
</span>
{isItemExpanded ? (
<ChevronUpIcon className="h-4 w-4 text-gray-400" />
) : (
<ChevronDownIcon className="h-4 w-4 text-gray-400" />
)}
</div>
</td>
</tr>
{isItemExpanded && (
<tr>
<td colSpan={4} className="px-4 py-3 bg-gray-50 border-l-4 border-red-500">
<div className="space-y-3">
<h4 className="font-semibold text-gray-900 mb-3">Detalhes da Despesa</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-600 font-medium">ID:</span>
<span className="ml-2 text-gray-900">{item.idDespesa}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Ano:</span>
<span className="ml-2 text-gray-900">{item.ano}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Turno:</span>
<span className="ml-2 text-gray-900">{item.turno}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Partido:</span>
<span className="ml-2 text-gray-900">{item.sgPartido}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Tipo Fornecedor:</span>
<span className="ml-2 text-gray-900">{item.tipoFornecedor || '-'}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Tipo Documento:</span>
<span className="ml-2 text-gray-900">{item.tipoDocumento || '-'}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Origem:</span>
<span className="ml-2 text-gray-900">{item.origemDespesa || '-'}</span>
</div>
{item.cpfFornecedor && (
<div>
<span className="text-gray-600 font-medium">CPF:</span>
<span className="ml-2 text-gray-900">{item.cpfFornecedor}</span>
</div>
)}
{item.cnpjFornecedor && (
<div>
<span className="text-gray-600 font-medium">CNPJ:</span>
<span className="ml-2 text-gray-900">{item.cnpjFornecedor}</span>
</div>
)}
{item.municipioFornecedor && (
<div>
<span className="text-gray-600 font-medium">Município:</span>
<span className="ml-2 text-gray-900">{item.municipioFornecedor}</span>
</div>
)}
</div>
{item.descricao && (
<div className="mt-3 pt-3 border-t border-gray-200">
<span className="text-gray-600 font-medium">Descrição Completa:</span>
<p className="ml-2 text-gray-900 mt-1">{item.descricao}</p>
</div>
)}
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
{/* Load More Button */}
{hasMore && (
<div className="p-4 border-t border-gray-200 bg-gray-50 text-center">
<button
onClick={() => loadMoreExpenseItems(year)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg transition-colors font-medium"
>
Carregar mais 50 itens ({yearExpenses.length - visibleCount} restantes)
</button>
</div>
)}
</div>
)}
</div>
);
})}
</div>
</>
) : (
<div className="text-center text-gray-500 py-6">
<div className="flex flex-col items-center">
<ArrowTrendingDownIcon className="h-10 w-10 text-gray-300 mb-2" />
<span>Nenhuma despesa encontrada</span>
</div>
</div>
)}
</div>
);
};
export default ExpenseSection;

View File

@ -0,0 +1,321 @@
import React, { useState, useMemo } from 'react';
import {
ChevronDownIcon,
ChevronUpIcon,
ArrowTrendingUpIcon,
CalendarIcon,
UserIcon,
BuildingOfficeIcon
} from '@heroicons/react/24/outline';
import { type CandidateIncome, type Income } from '../../../api';
interface IncomeSectionProps {
income: CandidateIncome | null;
isLoadingIncome: boolean;
}
const IncomeSection: React.FC<IncomeSectionProps> = ({
income,
isLoadingIncome
}) => {
const [expandedYears, setExpandedYears] = useState<Set<number>>(new Set());
const [visibleIncomeItems, setVisibleIncomeItems] = useState<Record<number, number>>({});
const [expandedIncomeItems, setExpandedIncomeItems] = useState<Set<string>>(new Set());
const formatCurrency = (value: number) => {
return new Intl.NumberFormat('pt-BR', {
style: 'currency',
currency: 'BRL'
}).format(value);
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('pt-BR');
};
const groupIncomeByYear = useMemo(() => {
const receitas = income?.receitas || [];
if (!receitas.length) return {};
return receitas.reduce((acc: Record<number, Income[]>, item: Income) => {
const year = item.ano;
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(item);
return acc;
}, {} as Record<number, Income[]>);
}, [income]);
// Sort items by value (descending) for each year
const sortedIncomeByYear = useMemo(() => {
const sorted: Record<number, Income[]> = {};
Object.keys(groupIncomeByYear).forEach(year => {
const yearNum = Number(year);
sorted[yearNum] = [...groupIncomeByYear[yearNum]].sort((a, b) => b.valor - a.valor);
});
return sorted;
}, [groupIncomeByYear]);
const sortedIncomeYears = useMemo(() => {
return Object.keys(sortedIncomeByYear).map(Number).sort((a, b) => b - a);
}, [sortedIncomeByYear]);
const getTotalIncome = () => {
const receitas = income?.receitas || [];
return receitas.reduce((total: number, item: Income) => total + item.valor, 0);
};
const getTotalForIncomeYear = (yearIncome: Income[]) => {
return yearIncome.reduce((total, item) => total + item.valor, 0);
};
const getVisibleIncomeItemsForYear = (year: number) => {
return visibleIncomeItems[year] || 50;
};
const loadMoreIncomeItems = (year: number) => {
setVisibleIncomeItems(prev => ({
...prev,
[year]: (prev[year] || 50) + 50
}));
};
const toggleYear = (year: number) => {
setExpandedYears(prev => {
const newSet = new Set(prev);
if (newSet.has(year)) {
newSet.delete(year);
} else {
newSet.add(year);
}
return newSet;
});
};
const toggleIncomeItem = (itemId: string) => {
setExpandedIncomeItems(prev => {
const newSet = new Set(prev);
if (newSet.has(itemId)) {
newSet.delete(itemId);
} else {
newSet.add(itemId);
}
return newSet;
});
};
const receitas = income?.receitas || [];
const isEmpty = receitas.length === 0;
return (
<div className="mb-8">
<div className="flex items-center mb-4">
<ArrowTrendingUpIcon className="h-6 w-6 text-green-600 mr-2" />
<h3 className="text-xl font-bold text-gray-900">Receitas</h3>
</div>
{isLoadingIncome ? (
<div className="flex items-center justify-center h-24">
<div className="animate-spin rounded-full h-6 w-6 border-4 border-green-600 border-t-transparent"></div>
</div>
) : !isEmpty ? (
<>
{/* Summary Card */}
<div className="bg-gradient-to-r from-green-50 to-blue-50 p-4 rounded-lg mb-4 border border-green-100">
<div className="flex items-center justify-between">
<span className="text-lg font-semibold text-gray-900">Total de Receitas</span>
<span className="text-xl font-bold text-green-600">
{formatCurrency(getTotalIncome())}
</span>
</div>
</div>
<div className="space-y-3">
{sortedIncomeYears.map((year) => {
const yearIncome = sortedIncomeByYear[year];
const isExpanded = expandedYears.has(year);
const visibleCount = getVisibleIncomeItemsForYear(year);
const hasMore = yearIncome.length > visibleCount;
const visibleItems = yearIncome.slice(0, visibleCount);
return (
<div key={`income-${year}`} className="border border-gray-200 rounded-lg overflow-hidden">
<button
onClick={() => toggleYear(year)}
className="w-full px-4 py-3 bg-gray-50 hover:bg-gray-100 flex items-center justify-between transition-colors"
>
<div className="flex items-center">
<CalendarIcon className="h-5 w-5 text-gray-600 mr-2" />
<span className="font-semibold text-gray-900">{year}</span>
<span className="ml-2 text-sm text-gray-600">
({yearIncome.length} {yearIncome.length === 1 ? 'item' : 'itens'})
</span>
</div>
<div className="flex items-center">
<span className="font-bold text-green-600 mr-2">
{formatCurrency(getTotalForIncomeYear(yearIncome))}
</span>
{isExpanded ? (
<ChevronUpIcon className="h-5 w-5 text-gray-600" />
) : (
<ChevronDownIcon className="h-5 w-5 text-gray-600" />
)}
</div>
</button>
{isExpanded && (
<div className="bg-white">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-4 py-3 text-left font-medium text-gray-900">Data</th>
<th className="px-4 py-3 text-left font-medium text-gray-900">Doador</th>
<th className="px-4 py-3 text-left font-medium text-gray-900">Descrição</th>
<th className="px-4 py-3 text-right font-medium text-gray-900">Valor</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{visibleItems.map((item, index) => {
const itemId = `income-${item.ano}-${index}`;
const isItemExpanded = expandedIncomeItems.has(itemId);
return (
<React.Fragment key={itemId}>
<tr
className="hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => toggleIncomeItem(itemId)}
>
<td className="px-4 py-3 text-gray-900">
{formatDate(item.dataReceita)}
</td>
<td className="px-4 py-3">
<div className="flex items-center">
{item.cpfDoador ? (
<UserIcon className="h-4 w-4 text-gray-400 mr-2" />
) : (
<BuildingOfficeIcon className="h-4 w-4 text-gray-400 mr-2" />
)}
<span className="text-gray-900 font-medium">
{item.nomeDoador || item.nomeDoadorRFB || 'Doador não informado'}
</span>
</div>
</td>
<td className="px-4 py-3 text-gray-600 max-w-xs truncate">
{item.descricao || 'Descrição não informada'}
</td>
<td className="px-4 py-3 text-right font-semibold">
<div className="flex items-center justify-end">
<span className="text-green-600 mr-2">
{formatCurrency(item.valor)}
</span>
{isItemExpanded ? (
<ChevronUpIcon className="h-4 w-4 text-gray-400" />
) : (
<ChevronDownIcon className="h-4 w-4 text-gray-400" />
)}
</div>
</td>
</tr>
{isItemExpanded && (
<tr>
<td colSpan={4} className="px-4 py-3 bg-gray-50 border-l-4 border-green-500">
<div className="space-y-3">
<h4 className="font-semibold text-gray-900 mb-3">Detalhes da Receita</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-sm">
<div>
<span className="text-gray-600 font-medium">ID:</span>
<span className="ml-2 text-gray-900">{item.idReceita}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Ano:</span>
<span className="ml-2 text-gray-900">{item.ano}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Turno:</span>
<span className="ml-2 text-gray-900">{item.turno}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Fonte:</span>
<span className="ml-2 text-gray-900">{item.fonteReceita || '-'}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Origem:</span>
<span className="ml-2 text-gray-900">{item.origemReceita || '-'}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Natureza:</span>
<span className="ml-2 text-gray-900">{item.naturezaReceita || '-'}</span>
</div>
<div>
<span className="text-gray-600 font-medium">Espécie:</span>
<span className="ml-2 text-gray-900">{item.especieReceita || '-'}</span>
</div>
{item.cpfDoador && (
<div>
<span className="text-gray-600 font-medium">CPF:</span>
<span className="ml-2 text-gray-900">{item.cpfDoador}</span>
</div>
)}
{item.cnpjDoador && (
<div>
<span className="text-gray-600 font-medium">CNPJ:</span>
<span className="ml-2 text-gray-900">{item.cnpjDoador}</span>
</div>
)}
{item.municipioDoador && (
<div>
<span className="text-gray-600 font-medium">Município:</span>
<span className="ml-2 text-gray-900">{item.municipioDoador}</span>
</div>
)}
</div>
{item.descricao && (
<div className="mt-3 pt-3 border-t border-gray-200">
<span className="text-gray-600 font-medium">Descrição Completa:</span>
<p className="ml-2 text-gray-900 mt-1">{item.descricao}</p>
</div>
)}
</div>
</td>
</tr>
)}
</React.Fragment>
);
})}
</tbody>
</table>
</div>
{/* Load More Button */}
{hasMore && (
<div className="p-4 border-t border-gray-200 bg-gray-50 text-center">
<button
onClick={() => loadMoreIncomeItems(year)}
className="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium"
>
Carregar mais 50 itens ({yearIncome.length - visibleCount} restantes)
</button>
</div>
)}
</div>
)}
</div>
);
})}
</div>
</>
) : (
<div className="text-center text-gray-500 py-6">
<div className="flex flex-col items-center">
<ArrowTrendingUpIcon className="h-10 w-10 text-gray-300 mb-2" />
<span>Nenhuma receita encontrada</span>
</div>
</div>
)}
</div>
);
};
export default IncomeSection;

View File

@ -0,0 +1,51 @@
import React from 'react';
import { BanknotesIcon } from '@heroicons/react/24/outline';
import { type CandidateExpenses, type CandidateIncome } from '../../api';
import IncomeSection from './IncomeExpense/IncomeSection';
import ExpenseSection from './IncomeExpense/ExpenseSection';
interface IncomeExpenseComponentProps {
expenses: CandidateExpenses | null;
income: CandidateIncome | null;
isLoadingExpenses: boolean;
isLoadingIncome: boolean;
}
const IncomeExpenseComponent: React.FC<IncomeExpenseComponentProps> = ({
expenses,
income,
isLoadingExpenses,
isLoadingIncome
}) => {
return (
<div className="bg-white/95 backdrop-blur-sm rounded-xl shadow-lg hover:shadow-xl transform hover:scale-[1.01] transition-all duration-200 p-6">
<div className="flex items-center mb-6">
<BanknotesIcon className="h-8 w-8 text-green-600 mr-3" />
<h2 className="text-2xl font-bold text-gray-900">Receitas e Despesas</h2>
</div>
{/* Income Section */}
<IncomeSection
income={income}
isLoadingIncome={isLoadingIncome}
/>
{/* Separator */}
<div className="flex items-center my-8">
<div className="flex-grow border-t border-gray-300"></div>
<div className="flex-shrink-0 px-4">
<div className="w-3 h-3 bg-gray-300 rounded-full"></div>
</div>
<div className="flex-grow border-t border-gray-300"></div>
</div>
{/* Expenses Section */}
<ExpenseSection
expenses={expenses}
isLoadingExpenses={isLoadingExpenses}
/>
</div>
);
};
export default IncomeExpenseComponent;