import React, { useState, useEffect, useRef } from "react"; import { Download, Plus, ArrowUpDown, ArrowUp, ArrowDown, X, AlertCircle, Search, Upload, } from "lucide-react"; import { useNavigate } from "react-router-dom"; interface FinancialTransaction { id: string; fot: number; fot_id?: string; // Add fot_id data: string; // date string YYYY-MM-DD dataRaw?: string; // Optional raw date for editing curso: string; instituicao: string; anoFormatura: number; // or string label empresa: string; tipoEvento: string; tipoServico: string; nome: string; // professional_name endereco?: string; // Not in DB schema but in UI? Schema doesn't have address. We'll omit or map if needed. whatsapp: string; cpf: string; tabelaFree: string; valorFree: number; valorExtra: number; descricaoExtra: string; totalPagar: number; dataPgto: string; // date string pgtoOk: boolean; } const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8080"; const Finance: React.FC = () => { const navigate = useNavigate(); const [transactions, setTransactions] = useState([]); const [showAddModal, setShowAddModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [successMessage, setSuccessMessage] = useState(""); // Success message state const [selectedTransaction, setSelectedTransaction] = useState(null); const [sortConfig, setSortConfig] = useState<{ key: keyof FinancialTransaction; direction: "asc" | "desc"; } | null>(null); // API Data States const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [tiposEventos, setTiposEventos] = useState([]); const [tiposServicos, setTiposServicos] = useState([]); // Filters State (Moved up) const [filters, setFilters] = useState({ fot: "", data: "", evento: "", servico: "", nome: "", status: "", curso: "", instituicao: "", ano: "", empresa: "", }); // Pagination State const [page, setPage] = useState(1); const [limit, setLimit] = useState(50); const [total, setTotal] = useState(0); // Scroll Sync Refs const topScrollRef = useRef(null); const tableScrollRef = useRef(null); const handleScroll = (source: 'top' | 'table') => { if (source === 'top' && topScrollRef.current && tableScrollRef.current) { tableScrollRef.current.scrollLeft = topScrollRef.current.scrollLeft; } else if (source === 'table' && topScrollRef.current && tableScrollRef.current) { topScrollRef.current.scrollLeft = tableScrollRef.current.scrollLeft; } }; // Form State const [formData, setFormData] = useState>({ fot: 0, data: new Date().toISOString().split("T")[0], curso: "", instituicao: "", anoFormatura: new Date().getFullYear(), empresa: "", tipoEvento: "", tipoServico: "", nome: "", whatsapp: "", cpf: "", tabelaFree: "", valorFree: 0, valorExtra: 0, descricaoExtra: "", totalPagar: 0, dataPgto: "", pgtoOk: false, }); // Auto-fill state const [fotLoading, setFotLoading] = useState(false); const [fotFound, setFotFound] = useState(false); const [fotEvents, setFotEvents] = useState([]); // New state const [showEventSelector, setShowEventSelector] = useState(false); // FOT Search State const [fotQuery, setFotQuery] = useState(""); const [fotResults, setFotResults] = useState([]); const [showFotSuggestions, setShowFotSuggestions] = useState(false); // Professional Search State const [proQuery, setProQuery] = useState(""); const [proResults, setProResults] = useState([]); const [showProSuggestions, setShowProSuggestions] = useState(false); const [proFunctions, setProFunctions] = useState([]); // Functions of selected professional const [selectedProId, setSelectedProId] = useState(null); // Validations const validateCpf = (cpf: string) => { // Simple length check for now, can be enhanced return cpf.replace(/\D/g, "").length === 11; }; const loadTransactions = async () => { const token = localStorage.getItem("token"); if (!token) { setError("Usuário não autenticado"); return; } setLoading(true); try { const queryParams = new URLSearchParams({ page: page.toString(), limit: limit.toString(), 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 || "", }); const res = await fetch(`${API_BASE_URL}/api/finance?${queryParams.toString()}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if (res.status === 401) throw new Error("Não autorizado"); if (!res.ok) throw new Error("Falha ao carregar transações"); const result = await res.json(); const data = result.data || result; // Fallback if API returns array (e.g. filtered by FOT) const count = result.total || data.length; setTotal(count); // Map Backend DTO to Frontend Interface const mapped = (Array.isArray(data) ? data : []).map((item: any) => ({ id: item.id, fot: item.fot_numero || 0, data: item.data_cobranca ? new Date(item.data_cobranca).toLocaleDateString("pt-BR", {timeZone: "UTC"}) : "", dataRaw: item.data_cobranca ? item.data_cobranca.split("T")[0] : "", curso: item.curso_nome || "", instituicao: item.instituicao_nome || "", anoFormatura: item.ano_formatura || "", empresa: item.empresa_nome || "", tipoEvento: item.tipo_evento, tipoServico: item.tipo_servico, nome: item.professional_name, whatsapp: item.whatsapp, cpf: item.cpf, tabelaFree: item.tabela_free, valorFree: parseFloat(item.valor_free), valorExtra: parseFloat(item.valor_extra), descricaoExtra: item.descricao_extra, totalPagar: parseFloat(item.total_pagar), dataPgto: item.data_pagamento ? item.data_pagamento.split("T")[0] : "", pgtoOk: item.pgto_ok, })); setTransactions(mapped); } catch (err) { console.error(err); setError("Erro ao carregar dados."); } finally { setLoading(false); } }; const loadAuxiliaryData = async () => { const token = localStorage.getItem("token"); if (!token) return; try { const headers = { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" }; const [evRes, servRes] = await Promise.all([ fetch(`${API_BASE_URL}/api/tipos-eventos`, { headers }), fetch(`${API_BASE_URL}/api/tipos-servicos`, { headers }), ]); if (evRes.ok) setTiposEventos(await evRes.json()); if (servRes.ok) setTiposServicos(await servRes.json()); } catch (e) { console.error(e); } }; useEffect(() => { loadTransactions(); 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({ startDate: "", endDate: "", includeWeekends: true, }); const [showDateFilters, setShowDateFilters] = useState(false); // 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); } // 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 - 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. } // Applying filtered logic reuse // ... code kept essentially same ... // 2. Sort by FOT (desc) then Date (desc) to group FOTs // Default sort is grouped by FOT if (!sortConfig) { return result.sort((a, b) => { // Group by FOT (String comparison to handle "20000MG") const fotA = String(a.fot || ""); const fotB = String(b.fot || ""); if (fotA !== fotB) return fotB.localeCompare(fotA, undefined, { numeric: true }); // Secondary: Date return new Date(b.dataRaw || b.data).getTime() - new Date(a.dataRaw || a.data).getTime(); }); } // Custom sort if implemented return result.sort((a, b) => { // @ts-ignore const aValue = a[sortConfig.key]; // @ts-ignore const bValue = b[sortConfig.key]; if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1; if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1; return 0; }); }, [transactions, filters, sortConfig, dateFilters]); const handleSort = (key: keyof FinancialTransaction) => { let direction: "asc" | "desc" = "asc"; if (sortConfig && sortConfig.key === key && sortConfig.direction === "asc") { direction = "desc"; } setSortConfig({ key, direction }); }; const handleFotSearch = async (query: string) => { setFotQuery(query); // If user types numbers, list options if (query.length < 2) { setFotResults([]); setShowFotSuggestions(false); return; } const token = localStorage.getItem("token"); try { const res = await fetch(`${API_BASE_URL}/api/finance/fot-search?q=${query}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if(res.ok) { const data = await res.json(); setFotResults(data); setShowFotSuggestions(true); } } catch (e) { console.error(e); } }; const selectFot = (fot: any) => { setFotQuery(String(fot.fot)); setShowFotSuggestions(false); handleAutoFill(fot.fot); }; const handleAutoFill = async (fotNum: number) => { if (!fotNum) return; const token = localStorage.getItem("token"); if (!token) return; setFotLoading(true); setFotEvents([]); setShowEventSelector(false); try { const res = await fetch(`${API_BASE_URL}/api/finance/autofill?fot=${fotNum}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if (res.ok) { const data = await res.json(); setFormData(prev => ({ ...prev, curso: data.curso_nome, instituicao: data.empresa_nome, empresa: data.empresa_nome, anoFormatura: data.ano_formatura_label, fot: fotNum, })); setFotFound(true); // @ts-ignore const fotId = data.id; setFormData(prev => ({ ...prev, fot_id: fotId })); setFotQuery(String(fotNum)); // Ensure query matches found fot // Now fetch events const evRes = await fetch(`${API_BASE_URL}/api/finance/fot-events?fot_id=${fotId}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if (evRes.ok) { const events = await evRes.json(); if (events && events.length > 0) { setFotEvents(events); setShowEventSelector(true); } } } else { setFotFound(false); } } catch (error) { console.error(error); setFotFound(false); } finally { setFotLoading(false); } }; // Auto-Pricing Effect useEffect(() => { const fetchPrice = async () => { if (!formData.tipoEvento || !formData.tipoServico) return; // If editing existing transaction, maybe don't overwrite unless user changes something? // But for "Nova Transação", strictly overwrite. // Let's assume overwrite if price is 0 or user changed inputs. // Simplified: always fetch if inputs present. const token = localStorage.getItem("token"); if (!token) return; try { const res = await fetch(`${API_BASE_URL}/api/finance/price?event=${encodeURIComponent(formData.tipoEvento)}&service=${encodeURIComponent(formData.tipoServico)}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if (res.ok) { const data = await res.json(); if (data.valor !== undefined) { setFormData(prev => ({ ...prev, valorFree: data.valor })); } } } catch (e) { console.error(e); } }; fetchPrice(); }, [formData.tipoEvento, formData.tipoServico]); const handleProSearch = async (query: string) => { setProQuery(query); setFormData(prev => ({ ...prev, nome: query })); // Update name as typed // Allow empty query to list filtered professionals if Function is selected if (query.length < 3 && !formData.tipoServico) { setProResults([]); setShowProSuggestions(false); return; } const token = localStorage.getItem("token"); try { const fnParam = formData.tipoServico ? `&function=${encodeURIComponent(formData.tipoServico)}` : ""; const res = await fetch(`${API_BASE_URL}/api/finance/professionals?q=${query}${fnParam}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if(res.ok) { const data = await res.json(); setProResults(data); setShowProSuggestions(true); } } catch (e) { console.error(e); } }; const selectProfessional = (pro: any) => { // Parse functions let funcs = []; try { funcs = pro.functions ? (typeof pro.functions === 'string' ? JSON.parse(pro.functions) : pro.functions) : []; } catch(e) { funcs = []; } setProFunctions(funcs); setSelectedProId(pro.id); setFormData(prev => ({ ...prev, nome: pro.nome, whatsapp: pro.whatsapp, cpf: pro.cpf_cnpj_titular, // Default to first function if available, else empty tabelaFree: funcs.length > 0 ? funcs[0].nome : "", })); setProQuery(pro.nome); setShowProSuggestions(false); }; const selectEvent = (ev: any) => { setFormData(prev => ({ ...prev, tipoEvento: ev.tipo_evento_nome, // If event has date, we could pre-fill? User request suggests keeping it flexible or maybe they didn't ask explicitly. })); setShowEventSelector(false); }; const handleEdit = async (t: FinancialTransaction) => { setSelectedTransaction(t); setFormData({ id: t.id, fot_id: t.fot_id, data: t.dataRaw || t.data, // Use raw YYYY-MM-DD for input tipoEvento: t.tipoEvento, tipoServico: t.tipoServico, nome: t.nome, whatsapp: t.whatsapp, cpf: t.cpf, tabelaFree: t.tabelaFree, valorFree: t.valorFree, valorExtra: t.valorExtra, descricaoExtra: t.descricaoExtra, dataPgto: t.dataPgto, pgtoOk: t.pgtoOk, totalPagar: t.totalPagar, }); setFotFound(false); // Reset fotFound state for edit modal // Fetch FOT details if ID exists if (t.fot_id) { const token = localStorage.getItem("token"); if (!token) return; try { const res = await fetch(`${API_BASE_URL}/api/cadastro-fot/${t.fot_id}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if (res.ok) { const data = await res.json(); setFormData(prev => ({ ...prev, curso: data.curso_nome || "", instituicao: data.empresa_nome || data.instituicao || "", empresa: data.empresa_nome || "", anoFormatura: data.ano_formatura_label || "", fot: data.fot, })); // Update the search query state too setFotQuery(String(data.fot)); setFotFound(true); } } catch (err) { console.error("Error fetching FOT details for edit:", err); } } else if (t.fot) { // Fallback if no ID but we have the number setFotQuery(String(t.fot)); } // Fetch professional functions if professional name is present if (t.nome) { const token = localStorage.getItem("token"); if (!token) return; try { const res = await fetch(`${API_BASE_URL}/api/finance/professionals?q=${encodeURIComponent(t.nome)}`, { headers: { "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" } }); if (res.ok) { const data = await res.json(); const professional = data.find((p: any) => p.nome === t.nome); if (professional) { let funcs = []; try { funcs = professional.functions ? (typeof professional.functions === 'string' ? JSON.parse(professional.functions) : professional.functions) : []; } catch(e) { funcs = []; } setProFunctions(funcs); } } } catch (err) { console.error("Error fetching professional functions for edit:", err); } } setShowEditModal(true); }; const handleSmartSave = async () => { const token = localStorage.getItem("token"); if (!token) { alert("Login expirado"); return; } // 1. Duplicate Check // Duplicate if: Same FOT + Same Name + Same Date + Same Event Type (optional) // Let's use FOT + Name + Date as primary key const isDuplicate = transactions.some(t => Number(t.fot) === Number(formData.fot) && t.nome.trim().toLowerCase() === (formData.nome || "").trim().toLowerCase() && t.tipoEvento === formData.tipoEvento && // Event needed to distinguish pre-event vs formatura? (t.dataRaw === formData.data || t.data === new Date(formData.data || "").toLocaleDateString("pt-BR", {timeZone: "UTC"})) ); if (isDuplicate && !selectedTransaction) { // Only check on Create alert("Já existe um lançamento para este Profissional neste Evento e Data."); return; } // 2. Prepare Payload const payload = { fot_id: formData.fot_id, data_cobranca: formData.data, tipo_evento: formData.tipoEvento, tipo_servico: formData.tipoServico, professional_name: formData.nome, whatsapp: formData.whatsapp, cpf: formData.cpf, tabela_free: formData.tabelaFree, valor_free: formData.valorFree ? Number(formData.valorFree) : 0, valor_extra: formData.valorExtra ? Number(formData.valorExtra) : 0, descricao_extra: formData.descricaoExtra, total_pagar: (Number(formData.valorFree) || 0) + (Number(formData.valorExtra) || 0), data_pagamento: formData.dataPgto || null, pgto_ok: formData.pgtoOk }; try { let res; if (formData.id) { // Edit Mode res = await fetch(`${API_BASE_URL}/api/finance/${formData.id}`, { method: "PUT", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" }, body: JSON.stringify(payload) }); } else { // Create Mode res = await fetch(`${API_BASE_URL}/api/finance`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${token}`, "x-regiao": localStorage.getItem("photum_selected_region") || "SP" }, body: JSON.stringify(payload) }); } if (!res.ok) throw new Error("Erro ao salvar"); // 3. Post-Save Logic if (selectedTransaction) { // If editing, close modal setShowEditModal(false); setSelectedTransaction(null); setSuccessMessage("Transação atualizada com sucesso!"); } else { // If creating, KEEP OPEN and CLEAR professional data setSuccessMessage("Lançamento salvo! Pronto para o próximo."); setFormData(prev => ({ ...prev, // Keep Event Data fot: prev.fot, fot_id: prev.fot_id, curso: prev.curso, instituicao: prev.instituicao, empresa: prev.empresa, anoFormatura: prev.anoFormatura, data: prev.data, tipoEvento: prev.tipoEvento, // Clear Professional Data nome: "", whatsapp: "", cpf: "", tipoServico: "", tabelaFree: "", valorFree: 0, valorExtra: 0, descricaoExtra: "", totalPagar: 0, pgtoOk: false })); setProQuery(""); setSelectedProId(null); } // Clear success message after 3s setTimeout(() => setSuccessMessage(""), 3000); // Reload list background loadTransactions(); } catch (err) { alert("Erro ao salvar: " + err); } }; // Calculations useEffect(() => { const total = (Number(formData.valorFree) || 0) + (Number(formData.valorExtra) || 0); setFormData((prev) => ({ ...prev, totalPagar: total })); }, [formData.valorFree, formData.valorExtra]); return (

Extrato

Controle financeiro e transações

{/* Advanced Date Filters */}
{showDateFilters && (
setDateFilters({...dateFilters, startDate: e.target.value})} />
setDateFilters({...dateFilters, endDate: e.target.value})} />
{(dateFilters.startDate || dateFilters.endDate || !dateFilters.includeWeekends) && (
)}
)}
{/* Pagination Controls (Top) */}
Mostrando {transactions.length} de {total} registros
Página {page}
{/* List */}
{/* Top Scrollbar Sync */}
handleScroll('top')} className="overflow-x-auto w-full" >
handleScroll('table')} className="overflow-x-auto" > {["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 => ( ))} {loading && ( )} {loading && ( )} {!loading && sortedTransactions.map((t, index) => { const isNewFot = index > 0 && t.fot !== sortedTransactions[index - 1].fot; // Check if this is the last item of the group (or list) to show summary const isLastOfGroup = index === sortedTransactions.length - 1 || t.fot !== sortedTransactions[index + 1].fot; // Only show summary if sorted by default (which groups by FOT) or explicitly sorted by FOT const showSummary = isLastOfGroup && (!sortConfig || sortConfig.key === 'fot'); return ( handleEdit(t)} > {showSummary && ( )} ); })}
{h} {/* Filters */} {h === "FOT" && setFilters({...filters, fot: e.target.value})} />} {h === "Data Evento" && setFilters({...filters, data: e.target.value})} />} {h === "Curso" && setFilters({...filters, curso: e.target.value})} />} {h === "Instituição" && setFilters({...filters, instituicao: e.target.value})} />} {h === "Ano" && setFilters({...filters, ano: e.target.value})} />} {h === "Empresa" && setFilters({...filters, empresa: e.target.value})} />} {h === "Evento" && setFilters({...filters, evento: e.target.value})} />} {h === "Serviço" && setFilters({...filters, servico: e.target.value})} />} {h === "Nome" && setFilters({...filters, nome: e.target.value})} />} {h === "OK" && setFilters({...filters, status: e.target.value})} />}
Carregando...
Carregando...
{t.fot || "?"} {t.data} {t.curso} {t.instituicao} {t.anoFormatura} {t.empresa} {t.tipoEvento} {t.tipoServico} {t.nome} {t.whatsapp} {t.cpf} {t.tabelaFree} {t.valorFree?.toFixed(2)} {t.valorExtra?.toFixed(2)} {t.descricaoExtra} {t.totalPagar?.toFixed(2)} {(() => { try { if (!t.dataPgto) return "-"; const d = new Date(t.dataPgto); if (isNaN(d.getTime())) return "-"; return d.toLocaleDateString("pt-BR", {timeZone: "UTC"}); } catch (e) { return "-"; } })()} {t.pgtoOk ? Sim : Não}
Total FOT {t.fot}: {/* Calculate sum for this group */} {sortedTransactions .filter(tr => tr.fot === t.fot) .reduce((sum, curr) => sum + (curr.totalPagar || 0), 0) .toFixed(2)}
{sortedTransactions.length === 0 && !loading && (
Nenhuma transação encontrada.
)}
{/* Pagination Controls */}
Mostrando {transactions.length} de {total} registros
Página {page}
{/* Modal */} {(showAddModal || showEditModal) && (

{showAddModal ? "Nova Transação" : "Editar Transação"}

{/* Auto-fill Section */}
handleFotSearch(e.target.value)} onBlur={() => setTimeout(() => setShowFotSuggestions(false), 200)} /> {showFotSuggestions && fotResults && fotResults.length > 0 && (
{fotResults.map(f => (
selectFot(f)} >
FOT: {f.fot}
{f.curso_nome} | {f.empresa_nome}
{f.ano_formatura_label}
))}
)}
{fotLoading && Buscando...}
{fotFound && (
Curso: {formData.curso} Inst: {formData.instituicao} Ano: {formData.anoFormatura}
{fotEvents.length > 0 && (

Eventos encontrados:

{fotEvents.map(ev => ( ))}
)}
)}
{/* Data */}
setFormData({...formData, data: e.target.value})} />
{/* Tipo Evento */}
{/* Tipo Serviço */}
{/* Professional Info */}
handleProSearch(e.target.value)} onBlur={() => setTimeout(() => setShowProSuggestions(false), 200)} // Delay to allow click placeholder="Digite para buscar..." /> {showProSuggestions && proResults && proResults.length > 0 && (
{proResults.map(p => (
selectProfessional(p)} > {p.nome} ({p.funcao_nome})
))}
)}
setFormData({...formData, whatsapp: e.target.value})} />
setFormData({...formData, cpf: e.target.value})} />
{/* Values */}
{proFunctions.length > 0 ? ( ) : ( setFormData({...formData, tabelaFree: e.target.value})} /> )}
setFormData({...formData, valorFree: parseFloat(e.target.value)})} />
setFormData({...formData, valorExtra: parseFloat(e.target.value)})} />
setFormData({...formData, descricaoExtra: e.target.value})} />
{/* Payment */}
setFormData({...formData, dataPgto: e.target.value})} />
Total a Pagar R$ {formData.totalPagar?.toFixed(2)}
{successMessage && ( {successMessage} )}
)}
); }; export default Finance;