photum/frontend/pages/PhotographerFinance.tsx
João Vitor 7fc96d77d2 feat: add photographer finance page and UI improvements
- 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
2025-12-12 16:26:12 -03:00

401 lines
14 KiB
TypeScript

import React, { useState, useEffect, useMemo } from "react";
import {
Download,
ArrowUpDown,
ArrowUp,
ArrowDown,
AlertCircle,
} from "lucide-react";
interface PhotographerPayment {
id: string;
data: string;
nomeEvento: string;
tipoEvento: string;
empresa: string;
valorRecebido: number;
dataPagamento: string;
statusPagamento: "Pago" | "Pendente" | "Atrasado";
}
type SortField = keyof PhotographerPayment | null;
type SortDirection = "asc" | "desc" | null;
const PhotographerFinance: React.FC = () => {
const [payments, setPayments] = useState<PhotographerPayment[]>([
{
id: "1",
data: "2025-11-15",
nomeEvento: "Formatura Medicina UFPR 2025",
tipoEvento: "Formatura",
empresa: "PhotoPro Studio",
valorRecebido: 1500.0,
dataPagamento: "2025-11-20",
statusPagamento: "Pago",
},
{
id: "2",
data: "2025-11-18",
nomeEvento: "Formatura Direito PUC-PR 2025",
tipoEvento: "Formatura",
empresa: "PhotoPro Studio",
valorRecebido: 1200.0,
dataPagamento: "2025-11-25",
statusPagamento: "Pago",
},
{
id: "3",
data: "2025-12-01",
nomeEvento: "Formatura Engenharia UTFPR 2025",
tipoEvento: "Formatura",
empresa: "Lens & Art",
valorRecebido: 1800.0,
dataPagamento: "2025-12-15",
statusPagamento: "Pendente",
},
]);
const [sortField, setSortField] = useState<SortField>(null);
const [sortDirection, setSortDirection] = useState<SortDirection>(null);
const [apiError, setApiError] = useState<string>("");
// Load API data
useEffect(() => {
loadApiData();
}, []);
const loadApiData = async () => {
try {
// TODO: Implementar chamada real da API
// const response = await fetch("http://localhost:3000/api/photographer/payments");
// const data = await response.json();
// setPayments(data);
} catch (error) {
console.error("Erro ao carregar pagamentos:", error);
setApiError(
"Não foi possível carregar os dados da API. Usando dados de exemplo."
);
}
};
// Sorting logic
const handleSort = (field: keyof PhotographerPayment) => {
if (sortField === field) {
if (sortDirection === "asc") {
setSortDirection("desc");
} else if (sortDirection === "desc") {
setSortDirection(null);
setSortField(null);
}
} else {
setSortField(field);
setSortDirection("asc");
}
};
const getSortIcon = (field: keyof PhotographerPayment) => {
if (sortField !== field) {
return (
<ArrowUpDown
size={16}
className="opacity-0 group-hover:opacity-50 transition-opacity"
/>
);
}
if (sortDirection === "asc") {
return <ArrowUp size={16} className="text-brand-gold" />;
}
if (sortDirection === "desc") {
return <ArrowDown size={16} className="text-brand-gold" />;
}
return (
<ArrowUpDown
size={16}
className="opacity-0 group-hover:opacity-50 transition-opacity"
/>
);
};
const sortedPayments = useMemo(() => {
if (!sortField || !sortDirection) return payments;
return [...payments].sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (aValue === null || aValue === undefined) return 1;
if (bValue === null || bValue === undefined) return -1;
let comparison = 0;
if (typeof aValue === "string" && typeof bValue === "string") {
comparison = aValue.localeCompare(bValue);
} else if (typeof aValue === "number" && typeof bValue === "number") {
comparison = aValue - bValue;
} else if (typeof aValue === "boolean" && typeof bValue === "boolean") {
comparison = aValue === bValue ? 0 : aValue ? 1 : -1;
}
return sortDirection === "asc" ? comparison : -comparison;
});
}, [payments, sortField, sortDirection]);
// Export to CSV
const handleExport = () => {
const headers = [
"Data Evento",
"Nome Evento",
"Tipo Evento",
"Empresa",
"Valor Recebido",
"Data Pagamento",
"Status",
];
const csvContent = [
headers.join(","),
...sortedPayments.map((p) =>
[
p.data,
`"${p.nomeEvento}"`,
p.tipoEvento,
p.empresa,
p.valorRecebido.toFixed(2),
p.dataPagamento,
p.statusPagamento,
].join(",")
),
].join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `meus_pagamentos_${Date.now()}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Calculate totals
const totalRecebido = sortedPayments.reduce(
(sum, p) => sum + p.valorRecebido,
0
);
const totalPago = sortedPayments
.filter((p) => p.statusPagamento === "Pago")
.reduce((sum, p) => sum + p.valorRecebido, 0);
const totalPendente = sortedPayments
.filter((p) => p.statusPagamento === "Pendente")
.reduce((sum, p) => sum + p.valorRecebido, 0);
const getStatusBadge = (status: string) => {
const statusColors = {
Pago: "bg-green-100 text-green-800",
Pendente: "bg-yellow-100 text-yellow-800",
Atrasado: "bg-red-100 text-red-800",
};
return (
<span
className={`px-2 py-1 rounded-full text-xs font-medium ${
statusColors[status as keyof typeof statusColors] ||
"bg-gray-100 text-gray-800"
}`}
>
{status}
</span>
);
};
return (
<div className="min-h-screen bg-gray-50 pt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900">Meus Pagamentos</h1>
<p className="mt-2 text-gray-600">
Visualize todos os pagamentos recebidos pelos eventos fotografados
</p>
</div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white rounded-lg shadow p-6">
<p className="text-sm text-gray-600 mb-1">Total Recebido</p>
<p className="text-2xl font-bold text-gray-900">
R$ {totalRecebido.toFixed(2)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-sm text-gray-600 mb-1">Pagamentos Confirmados</p>
<p className="text-2xl font-bold text-green-600">
R$ {totalPago.toFixed(2)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<p className="text-sm text-gray-600 mb-1">Pagamentos Pendentes</p>
<p className="text-2xl font-bold text-yellow-600">
R$ {totalPendente.toFixed(2)}
</p>
</div>
</div>
{/* Main Card */}
<div className="bg-white rounded-lg shadow">
{/* Actions Bar */}
<div className="px-6 py-4 border-b border-gray-200">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">
Histórico de Pagamentos
</h2>
<button
onClick={handleExport}
className="flex items-center gap-2 px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200 transition-colors"
>
<Download size={20} />
Exportar
</button>
</div>
</div>
{apiError && (
<div className="mx-6 mt-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
<AlertCircle
className="text-yellow-600 flex-shrink-0 mt-0.5"
size={20}
/>
<div className="flex-1">
<p className="text-sm font-medium text-yellow-800">Aviso</p>
<p className="text-sm text-yellow-700 mt-1">{apiError}</p>
</div>
</div>
)}
{/* Table */}
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("data")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Data Evento
{getSortIcon("data")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("nomeEvento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Nome Evento
{getSortIcon("nomeEvento")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("tipoEvento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Tipo Evento
{getSortIcon("tipoEvento")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("empresa")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Empresa
{getSortIcon("empresa")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("valorRecebido")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Valor Recebido
{getSortIcon("valorRecebido")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("dataPagamento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Data Pagamento
{getSortIcon("dataPagamento")}
</button>
</th>
<th className="px-4 py-3 text-left">
<button
onClick={() => handleSort("statusPagamento")}
className="group flex items-center gap-2 text-xs font-medium text-gray-700 hover:text-gray-900"
>
Status
{getSortIcon("statusPagamento")}
</button>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{sortedPayments.length === 0 ? (
<tr>
<td
colSpan={7}
className="px-4 py-8 text-center text-gray-500"
>
Nenhum pagamento encontrado
</td>
</tr>
) : (
sortedPayments.map((payment) => (
<tr
key={payment.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{new Date(payment.data).toLocaleDateString("pt-BR")}
</td>
<td className="px-4 py-3 text-sm">
{payment.nomeEvento}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{payment.tipoEvento}
</td>
<td className="px-4 py-3 text-sm">{payment.empresa}</td>
<td className="px-4 py-3 whitespace-nowrap text-sm font-medium text-green-600">
R$ {payment.valorRecebido.toFixed(2)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{new Date(payment.dataPagamento).toLocaleDateString(
"pt-BR"
)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm">
{getStatusBadge(payment.statusPagamento)}
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Footer */}
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
<p className="text-sm text-gray-600">
Total de pagamentos: {sortedPayments.length}
</p>
</div>
</div>
</div>
</div>
);
};
export default PhotographerFinance;