- Add photographer finance page at /meus-pagamentos with payment history table - Remove university management page and related routes - Update Finance and UserApproval pages with consistent spacing and typography - Fix Dashboard background color to match other pages (bg-gray-50) - Standardize navbar logo sizing across all pages - Change institution field in course form from dropdown to text input - Add year and semester fields for university graduation dates - Improve header spacing on all pages to pt-20 sm:pt-24 md:pt-28 lg:pt-32 - Apply font-serif styling consistently across page headers
995 lines
36 KiB
TypeScript
995 lines
36 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Download,
|
|
Plus,
|
|
ArrowUpDown,
|
|
ArrowUp,
|
|
ArrowDown,
|
|
X,
|
|
AlertCircle,
|
|
} from "lucide-react";
|
|
|
|
interface FinancialTransaction {
|
|
id: string;
|
|
fot: number;
|
|
data: string;
|
|
curso: string;
|
|
instituicao: string;
|
|
anoFormatura: number;
|
|
empresa: string;
|
|
tipoEvento: string;
|
|
tipoServico: string;
|
|
nome: string;
|
|
endereco: string;
|
|
whatsapp: string;
|
|
cpf: string;
|
|
tabelaFree: string;
|
|
valorFree: number;
|
|
valorExtra: number;
|
|
descricaoExtra: string;
|
|
totalPagar: number;
|
|
dataPgto: string;
|
|
pgtoOk: boolean;
|
|
}
|
|
|
|
const Finance: React.FC = () => {
|
|
const [transactions, setTransactions] = useState<FinancialTransaction[]>([
|
|
{
|
|
id: "1",
|
|
fot: 12345,
|
|
data: "2025-11-15",
|
|
curso: "Medicina",
|
|
instituicao: "UFPR",
|
|
anoFormatura: 2025,
|
|
empresa: "PhotoPro Studio",
|
|
tipoEvento: "Formatura",
|
|
tipoServico: "Fotografia Completa",
|
|
nome: "Ana Paula Silva",
|
|
endereco: "Rua das Flores, 123 - Curitiba/PR",
|
|
whatsapp: "(41) 99999-1234",
|
|
cpf: "123.456.789-00",
|
|
tabelaFree: "Pacote Premium",
|
|
valorFree: 5000.0,
|
|
valorExtra: 1500.0,
|
|
descricaoExtra: "Álbum adicional + drone",
|
|
totalPagar: 6500.0,
|
|
dataPgto: "2025-12-01",
|
|
pgtoOk: true,
|
|
},
|
|
{
|
|
id: "2",
|
|
fot: 12346,
|
|
data: "2025-11-20",
|
|
curso: "Direito",
|
|
instituicao: "PUC-PR",
|
|
anoFormatura: 2025,
|
|
empresa: "Lens & Art",
|
|
tipoEvento: "Formatura",
|
|
tipoServico: "Fotografia + Vídeo",
|
|
nome: "Carlos Eduardo",
|
|
endereco: "Av. Brasil, 456 - Curitiba/PR",
|
|
whatsapp: "(41) 98888-5678",
|
|
cpf: "987.654.321-00",
|
|
tabelaFree: "Pacote Standard",
|
|
valorFree: 4000.0,
|
|
valorExtra: 800.0,
|
|
descricaoExtra: "Ensaio pré-formatura",
|
|
totalPagar: 4800.0,
|
|
dataPgto: "2025-12-10",
|
|
pgtoOk: false,
|
|
},
|
|
]);
|
|
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [selectedTransaction, setSelectedTransaction] =
|
|
useState<FinancialTransaction | null>(null);
|
|
const [sortConfig, setSortConfig] = useState<{
|
|
key: keyof FinancialTransaction;
|
|
direction: "asc" | "desc";
|
|
} | null>(null);
|
|
|
|
// Estados para dados da API
|
|
const [cursos, setCursos] = useState<any[]>([]);
|
|
const [instituicoes, setInstituicoes] = useState<any[]>([]);
|
|
const [empresas, setEmpresas] = useState<any[]>([]);
|
|
const [tiposEventos, setTiposEventos] = useState<any[]>([]);
|
|
const [tiposServicos, setTiposServicos] = useState<any[]>([]);
|
|
const [apiError, setApiError] = useState<string>("");
|
|
const [loadingApi, setLoadingApi] = useState(false);
|
|
|
|
// Form state
|
|
const [formData, setFormData] = useState<Partial<FinancialTransaction>>({
|
|
fot: 0,
|
|
data: "",
|
|
curso: "",
|
|
instituicao: "",
|
|
anoFormatura: new Date().getFullYear(),
|
|
empresa: "",
|
|
tipoEvento: "",
|
|
tipoServico: "",
|
|
nome: "",
|
|
endereco: "",
|
|
whatsapp: "",
|
|
cpf: "",
|
|
tabelaFree: "",
|
|
valorFree: 0,
|
|
valorExtra: 0,
|
|
descricaoExtra: "",
|
|
totalPagar: 0,
|
|
dataPgto: "",
|
|
pgtoOk: false,
|
|
});
|
|
|
|
// Carregar dados da API
|
|
const loadApiData = async () => {
|
|
setLoadingApi(true);
|
|
setApiError("");
|
|
try {
|
|
const API_BASE_URL =
|
|
import.meta.env.VITE_API_URL || "http://localhost:3000";
|
|
|
|
// Carregar cursos
|
|
try {
|
|
const cursosRes = await fetch(`${API_BASE_URL}/api/cursos`);
|
|
if (cursosRes.ok) {
|
|
const cursosData = await cursosRes.json();
|
|
setCursos(cursosData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar cursos:", error);
|
|
}
|
|
|
|
// Carregar instituições (empresas cadastradas)
|
|
try {
|
|
const instRes = await fetch(`${API_BASE_URL}/api/empresas`);
|
|
if (instRes.ok) {
|
|
const instData = await instRes.json();
|
|
setInstituicoes(instData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar instituições:", error);
|
|
}
|
|
|
|
// Carregar empresas
|
|
try {
|
|
const empRes = await fetch(`${API_BASE_URL}/api/empresas`);
|
|
if (empRes.ok) {
|
|
const empData = await empRes.json();
|
|
setEmpresas(empData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar empresas:", error);
|
|
}
|
|
|
|
// Carregar tipos de eventos
|
|
try {
|
|
const evRes = await fetch(`${API_BASE_URL}/api/tipos-eventos`);
|
|
if (evRes.ok) {
|
|
const evData = await evRes.json();
|
|
setTiposEventos(evData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar tipos de eventos:", error);
|
|
}
|
|
|
|
// Carregar tipos de serviços
|
|
try {
|
|
const servRes = await fetch(`${API_BASE_URL}/api/tipos-servicos`);
|
|
if (servRes.ok) {
|
|
const servData = await servRes.json();
|
|
setTiposServicos(servData);
|
|
}
|
|
} catch (error) {
|
|
console.error("Erro ao carregar tipos de serviços:", error);
|
|
}
|
|
|
|
// Se todos falharam, mostrar erro
|
|
if (
|
|
cursos.length === 0 &&
|
|
instituicoes.length === 0 &&
|
|
empresas.length === 0 &&
|
|
tiposEventos.length === 0 &&
|
|
tiposServicos.length === 0
|
|
) {
|
|
setApiError(
|
|
"Backend não está rodando. Alguns campos podem não estar disponíveis."
|
|
);
|
|
}
|
|
} catch (error) {
|
|
setApiError(
|
|
"Backend não está rodando. Alguns campos podem não estar disponíveis."
|
|
);
|
|
} finally {
|
|
setLoadingApi(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (showAddModal || showEditModal) {
|
|
loadApiData();
|
|
}
|
|
}, [showAddModal, showEditModal]);
|
|
|
|
// Ordenação
|
|
const handleSort = (key: keyof FinancialTransaction) => {
|
|
if (sortConfig && sortConfig.key === key) {
|
|
// Se já está ordenando por este campo, alterna a ordem
|
|
if (sortConfig.direction === "asc") {
|
|
setSortConfig({ key, direction: "desc" });
|
|
} else {
|
|
// Se já está descendente, remove a ordenação
|
|
setSortConfig(null);
|
|
}
|
|
} else {
|
|
// Novo campo, começa com ordem ascendente
|
|
setSortConfig({ key, direction: "asc" });
|
|
}
|
|
};
|
|
|
|
const sortedTransactions = React.useMemo(() => {
|
|
let sortableTransactions = [...transactions];
|
|
if (sortConfig !== null) {
|
|
sortableTransactions.sort((a, b) => {
|
|
const aValue = a[sortConfig.key];
|
|
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;
|
|
});
|
|
}
|
|
return sortableTransactions;
|
|
}, [transactions, sortConfig]);
|
|
|
|
const getSortIcon = (key: keyof FinancialTransaction) => {
|
|
if (sortConfig?.key !== key) {
|
|
return (
|
|
<ArrowUpDown
|
|
size={14}
|
|
className="opacity-0 group-hover:opacity-50 transition-opacity"
|
|
/>
|
|
);
|
|
}
|
|
if (sortConfig.direction === "asc") {
|
|
return <ArrowUp size={14} className="text-brand-gold" />;
|
|
}
|
|
return <ArrowDown size={14} className="text-brand-gold" />;
|
|
};
|
|
|
|
// Handlers
|
|
const handleAddTransaction = () => {
|
|
setFormData({
|
|
fot: 0,
|
|
data: "",
|
|
curso: "",
|
|
instituicao: "",
|
|
anoFormatura: new Date().getFullYear(),
|
|
empresa: "",
|
|
tipoEvento: "",
|
|
tipoServico: "",
|
|
nome: "",
|
|
endereco: "",
|
|
whatsapp: "",
|
|
cpf: "",
|
|
tabelaFree: "",
|
|
valorFree: 0,
|
|
valorExtra: 0,
|
|
descricaoExtra: "",
|
|
totalPagar: 0,
|
|
dataPgto: "",
|
|
pgtoOk: false,
|
|
});
|
|
setShowAddModal(true);
|
|
};
|
|
|
|
const handleEditTransaction = (transaction: FinancialTransaction) => {
|
|
setSelectedTransaction(transaction);
|
|
setFormData(transaction);
|
|
setShowEditModal(true);
|
|
};
|
|
|
|
const handleSaveTransaction = () => {
|
|
if (showEditModal && selectedTransaction) {
|
|
// Atualizar transação existente
|
|
setTransactions(
|
|
transactions.map((t) =>
|
|
t.id === selectedTransaction.id
|
|
? ({
|
|
...formData,
|
|
id: selectedTransaction.id,
|
|
} as FinancialTransaction)
|
|
: t
|
|
)
|
|
);
|
|
setShowEditModal(false);
|
|
} else {
|
|
// Adicionar nova transação
|
|
const newTransaction: FinancialTransaction = {
|
|
...formData,
|
|
id: Date.now().toString(),
|
|
} as FinancialTransaction;
|
|
setTransactions([...transactions, newTransaction]);
|
|
setShowAddModal(false);
|
|
}
|
|
setSelectedTransaction(null);
|
|
};
|
|
|
|
const handleExport = () => {
|
|
// Criar CSV
|
|
const headers = [
|
|
"FOT",
|
|
"Data",
|
|
"Curso",
|
|
"Instituição",
|
|
"Ano Formatura",
|
|
"Empresa",
|
|
"Tipo Evento",
|
|
"Tipo de Serviço",
|
|
"Nome",
|
|
"Endereço",
|
|
"WhatsApp",
|
|
"CPF",
|
|
"Tabela Free",
|
|
"Valor Free",
|
|
"Valor Extra",
|
|
"Descrição do Extra",
|
|
"Total a Pagar",
|
|
"Data Pgto",
|
|
"Pgto OK",
|
|
];
|
|
|
|
const csvContent = [
|
|
headers.join(","),
|
|
...transactions.map((t) =>
|
|
[
|
|
t.fot,
|
|
t.data,
|
|
t.curso,
|
|
t.instituicao,
|
|
t.anoFormatura,
|
|
t.empresa,
|
|
t.tipoEvento,
|
|
t.tipoServico,
|
|
t.nome,
|
|
`"${t.endereco}"`,
|
|
t.whatsapp,
|
|
t.cpf,
|
|
t.tabelaFree,
|
|
t.valorFree,
|
|
t.valorExtra,
|
|
`"${t.descricaoExtra}"`,
|
|
t.totalPagar,
|
|
t.dataPgto,
|
|
t.pgtoOk ? "Sim" : "Não",
|
|
].join(",")
|
|
),
|
|
].join("\n");
|
|
|
|
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
|
|
const link = document.createElement("a");
|
|
link.href = URL.createObjectURL(blob);
|
|
link.download = `financeiro_${new Date().toISOString().split("T")[0]}.csv`;
|
|
link.click();
|
|
};
|
|
|
|
const formatCurrency = (value: number) => {
|
|
return new Intl.NumberFormat("pt-BR", {
|
|
style: "currency",
|
|
currency: "BRL",
|
|
}).format(value);
|
|
};
|
|
|
|
const formatDate = (dateString: string) => {
|
|
if (!dateString) return "";
|
|
return new Date(dateString + "T00:00:00").toLocaleDateString("pt-BR");
|
|
};
|
|
|
|
// Atualizar total ao mudar valores
|
|
useEffect(() => {
|
|
const total = (formData.valorFree || 0) + (formData.valorExtra || 0);
|
|
setFormData((prev) => ({ ...prev, totalPagar: total }));
|
|
}, [formData.valorFree, formData.valorExtra]);
|
|
|
|
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-[95%] mx-auto px-3 sm:px-4">
|
|
{/* Header */}
|
|
<div className="mb-6 sm:mb-8">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-serif font-bold text-brand-black">
|
|
Financeiro
|
|
</h1>
|
|
<p className="text-sm sm:text-base text-gray-600 mt-1">
|
|
Gestão de transações financeiras
|
|
</p>
|
|
</div>
|
|
<div className="flex gap-2 sm:gap-3">
|
|
<button
|
|
onClick={handleExport}
|
|
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-gray-600 text-white rounded-md hover:bg-gray-700 transition-colors font-medium text-xs sm:text-sm"
|
|
>
|
|
<Download size={16} className="sm:w-[18px] sm:h-[18px]" />
|
|
<span className="hidden sm:inline">Exportar</span>
|
|
</button>
|
|
<button
|
|
onClick={handleAddTransaction}
|
|
className="flex items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1.5 sm:py-2 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium text-xs sm:text-sm"
|
|
>
|
|
<Plus size={16} className="sm:w-[18px] sm:h-[18px]" />
|
|
<span className="sm:hidden">Nova Transação</span>
|
|
<span className="hidden sm:inline">Cadastrar Transação</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Tabela */}
|
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-x-auto">
|
|
<table className="w-full text-xs sm:text-sm min-w-[1200px]">
|
|
<thead className="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
{[
|
|
{ key: "fot", label: "FOT" },
|
|
{ key: "data", label: "Data" },
|
|
{ key: "curso", label: "Curso" },
|
|
{ key: "instituicao", label: "Instituição" },
|
|
{ key: "anoFormatura", label: "Ano Formatura" },
|
|
{ key: "empresa", label: "Empresa" },
|
|
{ key: "tipoEvento", label: "Tipo Evento" },
|
|
{ key: "tipoServico", label: "Tipo de Serviço" },
|
|
{ key: "nome", label: "Nome" },
|
|
{ key: "endereco", label: "Endereço" },
|
|
{ key: "whatsapp", label: "WhatsApp" },
|
|
{ key: "cpf", label: "CPF" },
|
|
{ key: "tabelaFree", label: "Tabela Free" },
|
|
{ key: "valorFree", label: "Valor Free" },
|
|
{ key: "valorExtra", label: "Valor Extra" },
|
|
{ key: "descricaoExtra", label: "Descrição do Extra" },
|
|
{ key: "totalPagar", label: "Total a Pagar" },
|
|
{ key: "dataPgto", label: "Data Pgto" },
|
|
{ key: "pgtoOk", label: "Pgto OK" },
|
|
].map((column) => (
|
|
<th
|
|
key={column.key}
|
|
onClick={() =>
|
|
handleSort(column.key as keyof FinancialTransaction)
|
|
}
|
|
className="px-4 py-3 text-left font-semibold text-gray-700 cursor-pointer hover:bg-gray-100 transition-colors whitespace-nowrap group"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{column.label}
|
|
{getSortIcon(column.key as keyof FinancialTransaction)}
|
|
</div>
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-gray-200">
|
|
{sortedTransactions.map((transaction) => (
|
|
<tr
|
|
key={transaction.id}
|
|
onClick={() => handleEditTransaction(transaction)}
|
|
className="hover:bg-gray-50 cursor-pointer transition-colors"
|
|
>
|
|
<td className="px-4 py-3 whitespace-nowrap font-medium">
|
|
{transaction.fot}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{formatDate(transaction.data)}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.curso}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.instituicao}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap text-center">
|
|
{transaction.anoFormatura}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.empresa}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.tipoEvento}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.tipoServico}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.nome}
|
|
</td>
|
|
<td className="px-4 py-3 max-w-xs truncate">
|
|
{transaction.endereco}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.whatsapp}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.cpf}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{transaction.tabelaFree}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
|
{formatCurrency(transaction.valorFree)}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
|
{formatCurrency(transaction.valorExtra)}
|
|
</td>
|
|
<td className="px-4 py-3 max-w-xs truncate">
|
|
{transaction.descricaoExtra}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap text-right font-semibold text-green-600">
|
|
{formatCurrency(transaction.totalPagar)}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap">
|
|
{formatDate(transaction.dataPgto)}
|
|
</td>
|
|
<td className="px-4 py-3 whitespace-nowrap text-center">
|
|
<span
|
|
className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
transaction.pgtoOk
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-red-100 text-red-800"
|
|
}`}
|
|
>
|
|
{transaction.pgtoOk ? "Sim" : "Não"}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{sortedTransactions.length === 0 && (
|
|
<div className="text-center py-12 text-gray-500">
|
|
Nenhuma transação cadastrada
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Modal Adicionar/Editar */}
|
|
{(showAddModal || showEditModal) && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-3 sm:p-4">
|
|
<div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div className="sticky top-0 bg-white border-b border-gray-200 p-4 sm:p-6 flex items-center justify-between">
|
|
<h2 className="text-lg sm:text-xl md:text-2xl font-bold">
|
|
{showEditModal ? "Editar Transação" : "Cadastrar Transação"}
|
|
</h2>
|
|
<button
|
|
onClick={() => {
|
|
setShowAddModal(false);
|
|
setShowEditModal(false);
|
|
setSelectedTransaction(null);
|
|
}}
|
|
className="text-gray-500 hover:text-gray-700"
|
|
>
|
|
<X size={20} className="sm:w-6 sm:h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
{apiError && (
|
|
<div className="mx-4 sm:mx-6 mt-4 sm:mt-6 p-3 sm:p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-2 sm:gap-3">
|
|
<AlertCircle
|
|
className="text-yellow-600 flex-shrink-0 mt-0.5"
|
|
size={18}
|
|
/>
|
|
<div className="flex-1">
|
|
<p className="text-xs sm:text-sm font-medium text-yellow-800">
|
|
Aviso
|
|
</p>
|
|
<p className="text-xs sm:text-sm text-yellow-700 mt-1">
|
|
{apiError}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="p-4 sm:p-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 sm:gap-4">
|
|
{/* FOT */}
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
|
FOT *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={formData.fot || ""}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
if (value.length <= 5) {
|
|
setFormData({ ...formData, fot: parseInt(value) || 0 });
|
|
}
|
|
}}
|
|
max={99999}
|
|
className="w-full px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
placeholder="Máx. 5 dígitos"
|
|
/>
|
|
</div>
|
|
|
|
{/* Data */}
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
|
Data *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={formData.data}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, data: e.target.value })
|
|
}
|
|
className="w-full px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Curso */}
|
|
<div>
|
|
<label className="block text-xs sm:text-sm font-medium text-gray-700 mb-1.5 sm:mb-2">
|
|
Curso *
|
|
</label>
|
|
<select
|
|
value={formData.curso}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, curso: e.target.value })
|
|
}
|
|
disabled={loadingApi}
|
|
className="w-full px-3 sm:px-4 py-1.5 sm:py-2 text-xs sm:text-sm border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi
|
|
? "Carregando..."
|
|
: cursos.length > 0
|
|
? "Selecione um curso"
|
|
: "Nenhum curso disponível"}
|
|
</option>
|
|
{cursos.map((curso) => (
|
|
<option key={curso.id} value={curso.nome}>
|
|
{curso.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Instituição */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Instituição *
|
|
</label>
|
|
<select
|
|
value={formData.instituicao}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, instituicao: e.target.value })
|
|
}
|
|
disabled={loadingApi}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi
|
|
? "Carregando..."
|
|
: instituicoes.length > 0
|
|
? "Selecione uma instituição"
|
|
: "Nenhuma instituição disponível"}
|
|
</option>
|
|
{instituicoes.map((inst) => (
|
|
<option key={inst.id} value={inst.nome}>
|
|
{inst.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Ano Formatura */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Ano Formatura *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={formData.anoFormatura}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
anoFormatura: parseInt(e.target.value),
|
|
})
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Empresa */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Empresa *
|
|
</label>
|
|
<select
|
|
value={formData.empresa}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, empresa: e.target.value })
|
|
}
|
|
disabled={loadingApi}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi
|
|
? "Carregando..."
|
|
: empresas.length > 0
|
|
? "Selecione uma empresa"
|
|
: "Nenhuma empresa disponível"}
|
|
</option>
|
|
{empresas.map((emp) => (
|
|
<option key={emp.id} value={emp.nome}>
|
|
{emp.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Tipo Evento */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tipo Evento *
|
|
</label>
|
|
<select
|
|
value={formData.tipoEvento}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, tipoEvento: e.target.value })
|
|
}
|
|
disabled={loadingApi}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi
|
|
? "Carregando..."
|
|
: tiposEventos.length > 0
|
|
? "Selecione um tipo"
|
|
: "Nenhum tipo de evento disponível"}
|
|
</option>
|
|
{tiposEventos.map((tipo) => (
|
|
<option key={tipo.id} value={tipo.nome}>
|
|
{tipo.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Tipo de Serviço */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tipo de Serviço *
|
|
</label>
|
|
<select
|
|
value={formData.tipoServico}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, tipoServico: e.target.value })
|
|
}
|
|
disabled={loadingApi}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent disabled:bg-gray-50 disabled:text-gray-500"
|
|
>
|
|
<option value="">
|
|
{loadingApi
|
|
? "Carregando..."
|
|
: tiposServicos.length > 0
|
|
? "Selecione um tipo"
|
|
: "Nenhum tipo de serviço disponível"}
|
|
</option>
|
|
{tiposServicos.map((tipo) => (
|
|
<option key={tipo.id} value={tipo.nome}>
|
|
{tipo.nome}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{/* Nome */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nome *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.nome}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, nome: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Endereço */}
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Endereço
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.endereco}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, endereco: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* WhatsApp */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
WhatsApp
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.whatsapp}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, whatsapp: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
placeholder="(00) 00000-0000"
|
|
/>
|
|
</div>
|
|
|
|
{/* CPF */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
CPF
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.cpf}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, cpf: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
placeholder="000.000.000-00"
|
|
/>
|
|
</div>
|
|
|
|
{/* Tabela Free */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Tabela Free
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formData.tabelaFree}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, tabelaFree: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Valor Free */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Valor Free
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={formData.valorFree}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
valorFree: parseFloat(e.target.value) || 0,
|
|
})
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Valor Extra */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Valor Extra
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={formData.valorExtra}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
valorExtra: parseFloat(e.target.value) || 0,
|
|
})
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Descrição do Extra */}
|
|
<div className="md:col-span-2">
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Descrição do Extra
|
|
</label>
|
|
<textarea
|
|
value={formData.descricaoExtra}
|
|
onChange={(e) =>
|
|
setFormData({
|
|
...formData,
|
|
descricaoExtra: e.target.value,
|
|
})
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* Total a Pagar */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Total a Pagar
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formatCurrency(formData.totalPagar || 0)}
|
|
disabled
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-700 font-semibold"
|
|
/>
|
|
</div>
|
|
|
|
{/* Data Pgto */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Data Pgto
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={formData.dataPgto}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, dataPgto: e.target.value })
|
|
}
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-brand-gold focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
{/* Pgto OK */}
|
|
<div className="md:col-span-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={formData.pgtoOk}
|
|
onChange={(e) =>
|
|
setFormData({ ...formData, pgtoOk: e.target.checked })
|
|
}
|
|
className="w-4 h-4 sm:w-5 sm:h-5 text-brand-gold focus:ring-brand-gold border-gray-300 rounded"
|
|
/>
|
|
<span className="text-xs sm:text-sm font-medium text-gray-700">
|
|
Pagamento Confirmado
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Botões */}
|
|
<div className="flex gap-2 sm:gap-3 mt-6 sm:mt-8">
|
|
<button
|
|
onClick={() => {
|
|
setShowAddModal(false);
|
|
setShowEditModal(false);
|
|
setSelectedTransaction(null);
|
|
}}
|
|
className="flex-1 px-3 sm:px-4 py-2 sm:py-3 bg-gray-200 text-gray-800 rounded-md hover:bg-gray-300 transition-colors font-medium text-xs sm:text-sm"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={handleSaveTransaction}
|
|
className="flex-1 px-3 sm:px-4 py-2 sm:py-3 bg-brand-gold text-white rounded-md hover:bg-[#a5bd2e] transition-colors font-medium text-xs sm:text-sm"
|
|
>
|
|
{showEditModal ? "Salvar" : "Cadastrar"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Finance;
|