feat(finance): implementa exportação csv, ordenação e totalização

- Substitui importação por exportação de dados em CSV
- Adiciona ordenação alfabética e por data nas colunas da tabela
- Exibe somatório total dos registros filtrados na interface e no export
- Corrige escopo de variáveis no useEffect de filtros
This commit is contained in:
NANDO9322 2026-02-09 19:42:22 -03:00
parent b445d69de2
commit 050c164286
5 changed files with 308 additions and 165 deletions

View file

@ -39,20 +39,28 @@ WHERE
($7::text = '' OR c.nome ILIKE '%' || $7 || '%') AND
($8::text = '' OR f.instituicao ILIKE '%' || $8 || '%') AND
($9::text = '' OR CAST(a.ano_semestre AS TEXT) ILIKE '%' || $9 || '%') AND
($10::text = '' OR e.nome ILIKE '%' || $10 || '%')
($10::text = '' OR e.nome ILIKE '%' || $10 || '%') AND
-- Date Range Logic
($11::text = '' OR t.data_cobranca >= CAST($11 AS DATE)) AND
($12::text = '' OR t.data_cobranca <= CAST($12 AS DATE)) AND
-- Weekend Logic
($13::boolean = true OR EXTRACT(DOW FROM t.data_cobranca) NOT IN (0, 6))
`
type CountTransactionsFilteredParams struct {
Regiao pgtype.Text `json:"regiao"`
Fot string `json:"fot"`
Data string `json:"data"`
Evento string `json:"evento"`
Servico string `json:"servico"`
Nome string `json:"nome"`
Curso string `json:"curso"`
Instituicao string `json:"instituicao"`
Ano string `json:"ano"`
Empresa string `json:"empresa"`
Regiao pgtype.Text `json:"regiao"`
Fot string `json:"fot"`
Data string `json:"data"`
Evento string `json:"evento"`
Servico string `json:"servico"`
Nome string `json:"nome"`
Curso string `json:"curso"`
Instituicao string `json:"instituicao"`
Ano string `json:"ano"`
Empresa string `json:"empresa"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
IncludeWeekends bool `json:"include_weekends"`
}
func (q *Queries) CountTransactionsFiltered(ctx context.Context, arg CountTransactionsFilteredParams) (int64, error) {
@ -67,6 +75,9 @@ func (q *Queries) CountTransactionsFiltered(ctx context.Context, arg CountTransa
arg.Instituicao,
arg.Ano,
arg.Empresa,
arg.StartDate,
arg.EndDate,
arg.IncludeWeekends,
)
var count int64
err := row.Scan(&count)
@ -523,24 +534,32 @@ WHERE
($9::text = '' OR c.nome ILIKE '%' || $9 || '%') AND
($10::text = '' OR f.instituicao ILIKE '%' || $10 || '%') AND
($11::text = '' OR CAST(a.ano_semestre AS TEXT) ILIKE '%' || $11 || '%') AND
($12::text = '' OR e.nome ILIKE '%' || $12 || '%')
($12::text = '' OR e.nome ILIKE '%' || $12 || '%') AND
-- Date Range Logic
($13::text = '' OR t.data_cobranca >= CAST($13 AS DATE)) AND
($14::text = '' OR t.data_cobranca <= CAST($14 AS DATE)) AND
-- Weekend Logic (0=Sunday, 6=Saturday)
($15::boolean = true OR EXTRACT(DOW FROM t.data_cobranca) NOT IN (0, 6))
ORDER BY t.data_cobranca DESC NULLS LAST
LIMIT $1 OFFSET $2
`
type ListTransactionsPaginatedFilteredParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
Regiao pgtype.Text `json:"regiao"`
Fot string `json:"fot"`
Data string `json:"data"`
Evento string `json:"evento"`
Servico string `json:"servico"`
Nome string `json:"nome"`
Curso string `json:"curso"`
Instituicao string `json:"instituicao"`
Ano string `json:"ano"`
Empresa string `json:"empresa"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
Regiao pgtype.Text `json:"regiao"`
Fot string `json:"fot"`
Data string `json:"data"`
Evento string `json:"evento"`
Servico string `json:"servico"`
Nome string `json:"nome"`
Curso string `json:"curso"`
Instituicao string `json:"instituicao"`
Ano string `json:"ano"`
Empresa string `json:"empresa"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
IncludeWeekends bool `json:"include_weekends"`
}
type ListTransactionsPaginatedFilteredRow struct {
@ -584,6 +603,9 @@ func (q *Queries) ListTransactionsPaginatedFiltered(ctx context.Context, arg Lis
arg.Instituicao,
arg.Ano,
arg.Empresa,
arg.StartDate,
arg.EndDate,
arg.IncludeWeekends,
)
if err != nil {
return nil, err

View file

@ -91,7 +91,12 @@ WHERE
(@curso::text = '' OR c.nome ILIKE '%' || @curso || '%') AND
(@instituicao::text = '' OR f.instituicao ILIKE '%' || @instituicao || '%') AND
(@ano::text = '' OR CAST(a.ano_semestre AS TEXT) ILIKE '%' || @ano || '%') AND
(@empresa::text = '' OR e.nome ILIKE '%' || @empresa || '%')
(@empresa::text = '' OR e.nome ILIKE '%' || @empresa || '%') AND
-- Date Range Logic
(@start_date::text = '' OR t.data_cobranca >= CAST(@start_date AS DATE)) AND
(@end_date::text = '' OR t.data_cobranca <= CAST(@end_date AS DATE)) AND
-- Weekend Logic (0=Sunday, 6=Saturday)
(@include_weekends::boolean = true OR EXTRACT(DOW FROM t.data_cobranca) NOT IN (0, 6))
ORDER BY t.data_cobranca DESC NULLS LAST
LIMIT $1 OFFSET $2;
@ -115,4 +120,9 @@ WHERE
(@curso::text = '' OR c.nome ILIKE '%' || @curso || '%') AND
(@instituicao::text = '' OR f.instituicao ILIKE '%' || @instituicao || '%') AND
(@ano::text = '' OR CAST(a.ano_semestre AS TEXT) ILIKE '%' || @ano || '%') AND
(@empresa::text = '' OR e.nome ILIKE '%' || @empresa || '%');
(@empresa::text = '' OR e.nome ILIKE '%' || @empresa || '%') AND
-- Date Range Logic
(@start_date::text = '' OR t.data_cobranca >= CAST(@start_date AS DATE)) AND
(@end_date::text = '' OR t.data_cobranca <= CAST(@end_date AS DATE)) AND
-- Weekend Logic
(@include_weekends::boolean = true OR EXTRACT(DOW FROM t.data_cobranca) NOT IN (0, 6));

View file

@ -210,18 +210,28 @@ func (h *Handler) List(c *gin.Context) {
instituicao := c.Query("instituicao")
ano := c.Query("ano")
empresa := c.Query("empresa")
startDate := c.Query("startDate")
endDate := c.Query("endDate")
includeWeekendsStr := c.Query("includeWeekends")
includeWeekends := true
if includeWeekendsStr == "false" {
includeWeekends = false
}
regiao := c.GetString("regiao")
list, count, err := h.service.ListPaginated(c.Request.Context(), int32(page), int32(limit), FilterParams{
Fot: fot,
Data: data,
Evento: evento,
Servico: servico,
Nome: nome,
Curso: curso,
Instituicao: instituicao,
Ano: ano,
Empresa: empresa,
Fot: fot,
Data: data,
Evento: evento,
Servico: servico,
Nome: nome,
Curso: curso,
Instituicao: instituicao,
Ano: ano,
Empresa: empresa,
StartDate: startDate,
EndDate: endDate,
IncludeWeekends: includeWeekends,
}, regiao)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})

View file

@ -93,15 +93,18 @@ func (s *Service) ListAll(ctx context.Context, regiao string) ([]generated.ListT
}
type FilterParams struct {
Fot string
Data string
Evento string
Servico string
Nome string
Curso string
Instituicao string
Ano string
Empresa string
Fot string
Data string
Evento string
Servico string
Nome string
Curso string
Instituicao string
Ano string
Empresa string
StartDate string
EndDate string
IncludeWeekends bool
}
func (s *Service) ListPaginated(ctx context.Context, page int32, limit int32, filters FilterParams, regiao string) ([]generated.ListTransactionsPaginatedFilteredRow, int64, error) {
@ -114,34 +117,40 @@ func (s *Service) ListPaginated(ctx context.Context, page int32, limit int32, fi
offset := (page - 1) * limit
rows, err := s.queries.ListTransactionsPaginatedFiltered(ctx, generated.ListTransactionsPaginatedFilteredParams{
Limit: limit,
Offset: offset,
Fot: filters.Fot,
Data: filters.Data,
Evento: filters.Evento,
Servico: filters.Servico,
Nome: filters.Nome,
Curso: filters.Curso,
Instituicao: filters.Instituicao,
Ano: filters.Ano,
Empresa: filters.Empresa,
Regiao: pgtype.Text{String: regiao, Valid: true},
Limit: limit,
Offset: offset,
Fot: filters.Fot,
Data: filters.Data,
Evento: filters.Evento,
Servico: filters.Servico,
Nome: filters.Nome,
Curso: filters.Curso,
Instituicao: filters.Instituicao,
Ano: filters.Ano,
Empresa: filters.Empresa,
StartDate: filters.StartDate,
EndDate: filters.EndDate,
IncludeWeekends: filters.IncludeWeekends,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
return nil, 0, err
}
count, err := s.queries.CountTransactionsFiltered(ctx, generated.CountTransactionsFilteredParams{
Fot: filters.Fot,
Data: filters.Data,
Evento: filters.Evento,
Servico: filters.Servico,
Nome: filters.Nome,
Curso: filters.Curso,
Instituicao: filters.Instituicao,
Ano: filters.Ano,
Empresa: filters.Empresa,
Regiao: pgtype.Text{String: regiao, Valid: true},
Fot: filters.Fot,
Data: filters.Data,
Evento: filters.Evento,
Servico: filters.Servico,
Nome: filters.Nome,
Curso: filters.Curso,
Instituicao: filters.Instituicao,
Ano: filters.Ano,
Empresa: filters.Empresa,
StartDate: filters.StartDate,
EndDate: filters.EndDate,
IncludeWeekends: filters.IncludeWeekends,
Regiao: pgtype.Text{String: regiao, Valid: true},
})
if err != nil {
count = 0

View file

@ -155,6 +155,11 @@ const Finance: React.FC = () => {
instituicao: filters.instituicao || "",
ano: filters.ano || "",
empresa: filters.empresa || "",
// Send Date Filters to Backend
startDate: dateFilters.startDate || "",
endDate: dateFilters.endDate || "",
includeWeekends: String(dateFilters.includeWeekends),
});
const res = await fetch(`${API_BASE_URL}/api/finance?${queryParams.toString()}`, {
@ -228,14 +233,7 @@ const Finance: React.FC = () => {
loadAuxiliaryData();
}, [page, limit]); // Refresh on page/limit change
// Debounce filter changes
useEffect(() => {
const timer = setTimeout(() => {
setPage(1); // Reset to page 1 on filter change
loadTransactions();
}, 500);
return () => clearTimeout(timer);
}, [filters]);
// Advanced date filters
const [dateFilters, setDateFilters] = useState({
@ -245,80 +243,45 @@ const Finance: React.FC = () => {
});
const [showDateFilters, setShowDateFilters] = useState(false);
// Debounce filter changes
useEffect(() => {
const timer = setTimeout(() => {
setPage(1); // Reset to page 1 on filter change
loadTransactions();
}, 500);
return () => clearTimeout(timer);
}, [filters, dateFilters]);
// Calculate filtered and sorted transactions
const sortedTransactions = React.useMemo(() => {
let result = [...transactions];
// 1. Filter
if (filters.fot) result = result.filter(t => String(t.fot).includes(filters.fot));
if (filters.data) result = result.filter(t => t.data.includes(filters.data));
if (filters.evento) result = result.filter(t => t.tipoEvento.toLowerCase().includes(filters.evento.toLowerCase()));
if (filters.servico) result = result.filter(t => t.tipoServico.toLowerCase().includes(filters.servico.toLowerCase()));
if (filters.nome) result = result.filter(t => t.nome.toLowerCase().includes(filters.nome.toLowerCase()));
if (filters.curso) result = result.filter(t => t.curso.toLowerCase().includes(filters.curso.toLowerCase()));
if (filters.instituicao) result = result.filter(t => t.instituicao.toLowerCase().includes(filters.instituicao.toLowerCase()));
if (filters.ano) result = result.filter(t => String(t.anoFormatura).includes(filters.ano));
if (filters.empresa) result = result.filter(t => t.empresa.toLowerCase().includes(filters.empresa.toLowerCase()));
if (filters.status) {
const s = filters.status.toLowerCase();
if (s === 'ok' || s === 'sim') result = result.filter(t => t.pgtoOk);
if (s === 'no' || s === 'nao' || s === 'não') result = result.filter(t => !t.pgtoOk);
}
// Filters are handled by Backend
// if (filters.fot) result = result.filter(t => String(t.fot).includes(filters.fot));
// if (filters.data) result = result.filter(t => t.data.includes(filters.data));
// if (filters.evento) result = result.filter(t => t.tipoEvento.toLowerCase().includes(filters.evento.toLowerCase()));
// if (filters.servico) result = result.filter(t => t.tipoServico.toLowerCase().includes(filters.servico.toLowerCase()));
// if (filters.nome) result = result.filter(t => t.nome.toLowerCase().includes(filters.nome.toLowerCase()));
// if (filters.curso) result = result.filter(t => t.curso.toLowerCase().includes(filters.curso.toLowerCase()));
// if (filters.instituicao) result = result.filter(t => t.instituicao.toLowerCase().includes(filters.instituicao.toLowerCase()));
// if (filters.ano) result = result.filter(t => String(t.anoFormatura).includes(filters.ano));
// if (filters.empresa) result = result.filter(t => t.empresa.toLowerCase().includes(filters.empresa.toLowerCase()));
// if (filters.status) {
// const s = filters.status.toLowerCase();
// if (s === 'ok' || s === 'sim') result = result.filter(t => t.pgtoOk);
// if (s === 'no' || s === 'nao' || s === 'não') result = result.filter(t => !t.pgtoOk);
// }
// Advanced date filters
if (dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) {
result = result.filter(t => {
// Parse date from dataRaw (YYYY-MM-DD) or data (DD/MM/YYYY)
let dateToCheck: Date;
if (t.dataRaw) {
dateToCheck = new Date(t.dataRaw);
} else {
// Parse DD/MM/YYYY
const parts = t.data.split('/');
if (parts.length === 3) {
dateToCheck = new Date(parseInt(parts[2]), parseInt(parts[1]) - 1, parseInt(parts[0]));
} else {
return true; // Keep if can't parse
}
}
// Check date range
if (dateFilters.startDate) {
const startDate = new Date(dateFilters.startDate);
if (dateToCheck < startDate) return false;
}
if (dateFilters.endDate) {
const endDate = new Date(dateFilters.endDate);
endDate.setHours(23, 59, 59, 999); // Include the entire end date
if (dateToCheck > endDate) return false;
}
// Check weekends
if (!dateFilters.includeWeekends) {
const dayOfWeek = dateToCheck.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) return false; // 0 = Sunday, 6 = Saturday
}
return true;
});
}
// Advanced date filters - Handled by Backend now
// if (dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) {
// // ... filtering logic removed ...
// }
// Advanced date filters - Custom Logic
if (dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) {
result = result.filter(t => {
// Logic handled above but ensure it covers empty dates if strict?
// Current implementation from previous read is fine, assuming it exists.
// Wait, previous read showed it was implemented. I will trust it or should I re-implement to be sure?
// The previous read showed the logic block I want to keep.
// I will inject the UI controls for it below.
return true;
});
// Actually, let's refine the filter logic block if needed.
// Based on File read, it seemed complete.
// I'll leave the logic alone and just add the UI inputs.
// Wait, I need to check if the filter inputs are actually rendered.
// Previous read stopped at line 600. I check render now.
}
// Advanced date filters - Custom Logic (Removed)
// if (dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) { ... }
// Applying filtered logic reuse
// ... code kept essentially same ...
@ -344,6 +307,17 @@ const Finance: React.FC = () => {
const aValue = a[sortConfig.key];
// @ts-ignore
const bValue = b[sortConfig.key];
// String comparison for specific text fields
if (['nome', 'instituicao', 'curso', 'empresa', 'tipoEvento', 'tipoServico'].includes(sortConfig.key)) {
const strA = String(aValue || "").toLowerCase();
const strB = String(bValue || "").toLowerCase();
if (strA < strB) return sortConfig.direction === "asc" ? -1 : 1;
if (strA > strB) return sortConfig.direction === "asc" ? 1 : -1;
return 0;
}
// Default comparison (numbers, booleans)
if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1;
return 0;
@ -748,10 +722,97 @@ const Finance: React.FC = () => {
// Calculations
useEffect(() => {
const total = (Number(formData.valorFree) || 0) + (Number(formData.valorExtra) || 0);
setFormData((prev) => ({ ...prev, totalPagar: total }));
}, [formData.valorFree, formData.valorExtra]);
const handleExportCSV = () => {
if (sortedTransactions.length === 0) {
alert("Nenhum dado para exportar.");
return;
}
// CSV Header
const headers = [
"FOT", "Data", "Evento", "Serviço", "Nome", "WhatsApp", "CPF",
"Curso", "Instituição", "Ano", "Empresa",
"Valor Free", "Valor Extra", "Total Pagar", "Data Pagamento", "Pgto OK"
];
// CSV Rows
const rows = sortedTransactions.map(t => [
t.fot,
t.data,
t.tipoEvento,
t.tipoServico,
`"${t.nome}"`, // Quote names to handle commas
t.whatsapp ? `="${t.whatsapp}"` : "", // Force string in Excel to avoid scientific notation
t.cpf ? `="${t.cpf}"` : "",
`"${t.curso}"`,
`"${t.instituicao}"`,
t.anoFormatura,
`"${t.empresa}"`,
t.valorFree.toFixed(2).replace('.', ','),
t.valorExtra.toFixed(2).replace('.', ','),
t.totalPagar.toFixed(2).replace('.', ','),
t.dataPgto,
t.pgtoOk ? "Sim" : "Não"
]);
// Summation Row
const totalValue = sortedTransactions.reduce((sum, t) => sum + t.totalPagar, 0);
const sumRow = [
"TOTAL", "", "", "", "", "", "", "", "", "", "", "", "",
totalValue.toFixed(2).replace('.', ','), "", ""
];
// Combine
const csvContent = [
headers.join(";"),
...rows.map(r => r.join(";")),
sumRow.join(";")
].join("\n");
// Create Blob with BOM for Excel UTF-8 support
const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
// Dynamic Filename
let filename = "extrato_financeiro";
if (filters.nome) {
filename += `_${filters.nome.trim().replace(/\s+/g, '_').toLowerCase()}`;
}
const formatDateFilename = (isoDate: string) => {
if (!isoDate) return "";
const [y, m, d] = isoDate.split("-");
return `${d}-${m}-${y}`;
};
if (dateFilters.startDate) {
filename += `_de_${formatDateFilename(dateFilters.startDate)}`;
}
if (dateFilters.endDate) {
filename += `_ate_${formatDateFilename(dateFilters.endDate)}`;
}
// Fallback or just append timestamp if no specific filters?
// Let's always append a short date/time or just date if no filters to avoid overwrites,
// but user asked for specific format. Let's stick to user request + date fallback if empty.
if (!filters.nome && !dateFilters.startDate && !dateFilters.endDate) {
const today = new Date().toLocaleDateString("pt-BR").split("/").join("-"); // DD-MM-YYYY
filename += `_${today}`;
}
filename += ".csv";
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Calculate Total for Display
const filteredTotal = sortedTransactions.reduce((acc, curr) => acc + curr.totalPagar, 0);
return (
<div className="min-h-screen bg-gray-50 pt-20 sm:pt-24 md:pt-28 lg:pt-32 pb-8 sm:pb-12">
<div className="max-w-[98%] mx-auto px-2">
@ -761,12 +822,12 @@ const Finance: React.FC = () => {
<p className="text-gray-500 text-sm">Controle financeiro e transações</p>
</div>
<div className="flex gap-2">
<button
onClick={() => navigate("/importacao?tab=financeiro")}
className="bg-blue-600 text-white px-4 py-2 rounded shadow hover:bg-blue-700 transition flex items-center gap-2"
>
<Upload size={18} /> Importar Dados
</button>
<button
onClick={handleExportCSV}
className="bg-green-600 text-white px-4 py-2 rounded shadow hover:bg-green-700 transition flex items-center gap-2"
>
<Download size={18} /> Exportar Dados
</button>
<button
onClick={() => {
setFormData({ // Clear form to initial state
@ -860,7 +921,8 @@ const Finance: React.FC = () => {
{/* Pagination Controls (Top) */}
<div className="flex justify-between items-center px-4 py-2 bg-gray-50 border-b rounded-t-lg">
<span className="text-xs text-gray-600">
Mostrando {transactions.length} de {total} registros
Mostrando {transactions.length} de {total} registros |
<span className="ml-2 font-bold text-gray-900">Total Filtrado: R$ {filteredTotal.toFixed(2).replace('.', ',')}</span>
</span>
<div className="flex gap-2 items-center">
<button
@ -900,24 +962,54 @@ const Finance: React.FC = () => {
<table className="w-full text-xs text-left whitespace-nowrap min-w-[1500px]">
<thead className="bg-gray-100 border-b">
<tr>
{["FOT", "Data Evento", "Curso", "Instituição", "Ano", "Empresa", "Evento", "Serviço", "Nome", "WhatsApp", "CPF", "Tab. Free", "V. Free", "V. Extra", "Desc. Extra", "Total", "Dt. Pgto", "OK"].map(h => (
<th key={h} className="px-3 py-2 font-semibold text-gray-700 align-top">
<div className="flex flex-col gap-1">
<span>{h}</span>
{/* Filters */}
{h === "FOT" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.fot} onChange={e => setFilters({...filters, fot: e.target.value})} />}
{h === "Data Evento" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.data} onChange={e => setFilters({...filters, data: e.target.value})} />}
{h === "Curso" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.curso} onChange={e => setFilters({...filters, curso: e.target.value})} />}
{h === "Instituição" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.instituicao} onChange={e => setFilters({...filters, instituicao: e.target.value})} />}
{h === "Ano" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.ano} onChange={e => setFilters({...filters, ano: e.target.value})} />}
{h === "Empresa" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.empresa} onChange={e => setFilters({...filters, empresa: e.target.value})} />}
{h === "Evento" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.evento} onChange={e => setFilters({...filters, evento: e.target.value})} />}
{h === "Serviço" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.servico} onChange={e => setFilters({...filters, servico: e.target.value})} />}
{h === "Nome" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.nome} onChange={e => setFilters({...filters, nome: e.target.value})} />}
{h === "OK" && <input className="w-full text-[10px] border rounded px-1" placeholder="Sim/Não" value={filters.status} onChange={e => setFilters({...filters, status: e.target.value})} />}
</div>
</th>
))}
{[
{ label: "FOT", key: "fot" },
{ label: "Data Evento", key: "data" },
{ label: "Curso", key: "curso" },
{ label: "Instituição", key: "instituicao" },
{ label: "Ano", key: "anoFormatura" },
{ label: "Empresa", key: "empresa" },
{ label: "Evento", key: "tipoEvento" },
{ label: "Serviço", key: "tipoServico" },
{ label: "Nome", key: "nome" },
{ label: "WhatsApp", key: "whatsapp" },
{ label: "CPF", key: "cpf" },
{ label: "Tab. Free", key: "tabelaFree" },
{ label: "V. Free", key: "valorFree" },
{ label: "V. Extra", key: "valorExtra" },
{ label: "Desc. Extra", key: "descricaoExtra" },
{ label: "Total", key: "totalPagar" },
{ label: "Dt. Pgto", key: "dataPgto" },
{ label: "OK", key: "pgtoOk" }
].map((h) => (
<th
key={h.label}
className="px-3 py-2 font-semibold text-gray-700 align-top cursor-pointer hover:bg-gray-200"
onClick={() => handleSort(h.key as keyof FinancialTransaction)}
>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-1">
<span>{h.label}</span>
{sortConfig?.key === h.key && (
<span>{sortConfig.direction === "asc" ? <ArrowUp size={12}/> : <ArrowDown size={12}/>}</span>
)}
</div>
{/* Filters - Stop Propagation to prevent sort when clicking input */}
<div onClick={(e) => e.stopPropagation()}>
{h.label === "FOT" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.fot} onChange={e => setFilters({...filters, fot: e.target.value})} />}
{h.label === "Data Evento" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.data} onChange={e => setFilters({...filters, data: e.target.value})} />}
{h.label === "Curso" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.curso} onChange={e => setFilters({...filters, curso: e.target.value})} />}
{h.label === "Instituição" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.instituicao} onChange={e => setFilters({...filters, instituicao: e.target.value})} />}
{h.label === "Ano" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.ano} onChange={e => setFilters({...filters, ano: e.target.value})} />}
{h.label === "Empresa" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.empresa} onChange={e => setFilters({...filters, empresa: e.target.value})} />}
{h.label === "Evento" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.evento} onChange={e => setFilters({...filters, evento: e.target.value})} />}
{h.label === "Serviço" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.servico} onChange={e => setFilters({...filters, servico: e.target.value})} />}
{h.label === "Nome" && <input className="w-full text-[10px] border rounded px-1" placeholder="Filtrar" value={filters.nome} onChange={e => setFilters({...filters, nome: e.target.value})} />}
{h.label === "OK" && <input className="w-full text-[10px] border rounded px-1" placeholder="Sim/Não" value={filters.status} onChange={e => setFilters({...filters, status: e.target.value})} />}
</div>
</div>
</th>
))}
</tr>
</thead>
<tbody className="divide-y relative">