photum/frontend/components/EventTable.tsx
NANDO9322 66c7306553 feat: implementa filtro de agenda por usuário e corrige exibição de detalhes
- Backend: Adiciona `user_id` na tabela agenda e implementa queries de filtro por role.
- Frontend(Context): Corrige dependência do `useEffect` para garantir busca correta ao logar.
- Frontend(Context): Melhora mapeamento de dados (Número FOT, Fallback de Nome, Formandos).
- Frontend(UI): Atualiza EventTable e Dashboard para exibir número FOT e dados vinculados corretamente.
- Frontend(Fix): Resolve erros de TypeScript no enum EventStatus.
2025-12-16 13:44:02 -03:00

304 lines
11 KiB
TypeScript

import React, { useState, useMemo } from "react";
import { EventData, EventStatus, UserRole } from "../types";
import { CheckCircle, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react";
import { STATUS_COLORS } from "../constants";
interface EventTableProps {
events: EventData[];
onEventClick: (event: EventData) => void;
onApprove?: (e: React.MouseEvent, eventId: string) => void;
userRole: UserRole;
}
type SortField =
| "fot"
| "date"
| "curso"
| "instituicao"
| "anoFormatura"
| "empresa"
| "type"
| "status";
type SortOrder = "asc" | "desc" | null;
export const EventTable: React.FC<EventTableProps> = ({
events,
onEventClick,
onApprove,
userRole,
}) => {
const canApprove =
userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN;
const [sortField, setSortField] = useState<SortField | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const handleSort = (field: SortField) => {
if (sortField === field) {
// Se já está ordenando por este campo, alterna a ordem
if (sortOrder === "asc") {
setSortOrder("desc");
} else if (sortOrder === "desc") {
setSortOrder(null);
setSortField(null);
}
} else {
// Novo campo, começa com ordem ascendente
setSortField(field);
setSortOrder("asc");
}
};
const sortedEvents = useMemo(() => {
if (!sortField || !sortOrder) {
return events;
}
const sorted = [...events].sort((a, b) => {
let aValue: any;
let bValue: any;
switch (sortField) {
case "fot":
aValue = (a as any).fotId || "";
bValue = (b as any).fotId || "";
break;
case "date":
aValue = new Date(a.date + "T00:00:00").getTime();
bValue = new Date(b.date + "T00:00:00").getTime();
break;
case "curso":
aValue = ((a as any).curso || "").toLowerCase();
bValue = ((b as any).curso || "").toLowerCase();
break;
case "instituicao":
aValue = ((a as any).instituicao || "").toLowerCase();
bValue = ((b as any).instituicao || "").toLowerCase();
break;
case "anoFormatura":
aValue = (a as any).anoFormatura || 0;
bValue = (b as any).anoFormatura || 0;
break;
case "empresa":
aValue = ((a as any).empresa || "").toLowerCase();
bValue = ((b as any).empresa || "").toLowerCase();
break;
case "type":
aValue = a.type.toLowerCase();
bValue = b.type.toLowerCase();
break;
case "status":
aValue = a.status.toLowerCase();
bValue = b.status.toLowerCase();
break;
default:
return 0;
}
if (aValue < bValue) return sortOrder === "asc" ? -1 : 1;
if (aValue > bValue) return sortOrder === "asc" ? 1 : -1;
return 0;
});
return sorted;
}, [events, sortField, sortOrder]);
const getSortIcon = (field: SortField) => {
if (sortField !== field) {
return (
<ArrowUpDown
size={14}
className="opacity-0 group-hover:opacity-50 transition-opacity"
/>
);
}
if (sortOrder === "asc") {
return <ArrowUp size={14} className="text-brand-gold" />;
}
return <ArrowDown size={14} className="text-brand-gold" />;
};
const formatDate = (date: string) => {
const eventDate = new Date(date + "T00:00:00");
return eventDate.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
};
const getStatusDisplay = (status: EventStatus) => {
const statusLabels: Record<EventStatus, string> = {
[EventStatus.PENDING_APPROVAL]: "Pendente",
[EventStatus.CONFIRMED]: "Confirmado",
[EventStatus.PLANNING]: "Planejamento",
[EventStatus.IN_PROGRESS]: "Em Andamento",
[EventStatus.DELIVERED]: "Entregue",
[EventStatus.ARCHIVED]: "Arquivado",
};
return statusLabels[status] || status;
};
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm">
<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 text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("fot")}
>
<div className="flex items-center gap-2">
FOT
{getSortIcon("fot")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("date")}
>
<div className="flex items-center gap-2">
Data
{getSortIcon("date")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("curso")}
>
<div className="flex items-center gap-2">
Curso
{getSortIcon("curso")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("instituicao")}
>
<div className="flex items-center gap-2">
Instituição
{getSortIcon("instituicao")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("anoFormatura")}
>
<div className="flex items-center gap-2">
Ano Formatura
{getSortIcon("anoFormatura")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("empresa")}
>
<div className="flex items-center gap-2">
Empresa
{getSortIcon("empresa")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("type")}
>
<div className="flex items-center gap-2">
Tipo Evento
{getSortIcon("type")}
</div>
</th>
<th
className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider cursor-pointer hover:bg-gray-100 transition-colors group"
onClick={() => handleSort("status")}
>
<div className="flex items-center gap-2">
Status
{getSortIcon("status")}
</div>
</th>
{canApprove && (
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-20">
Ações
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{sortedEvents.map((event) => (
<tr
key={event.id}
onClick={() => onEventClick(event)}
className="hover:bg-gray-50 cursor-pointer transition-colors"
>
<td className="px-4 py-3">
<span className="text-sm font-medium text-gray-900">
{event.fot || "-"}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-gray-600">
{formatDate(event.date)}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-gray-600">
{event.curso || "-"}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-gray-600">
{event.instituicao || "-"}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-gray-600">
{event.anoFormatura || "-"}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-gray-600">
{event.empresa || "-"}
</span>
</td>
<td className="px-4 py-3">
<span className="text-sm text-gray-600">{event.type}</span>
</td>
<td className="px-4 py-3">
<span
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${STATUS_COLORS[event.status] || "bg-gray-100 text-gray-800"
}`}
>
{getStatusDisplay(event.status)}
</span>
</td>
{canApprove && (
<td
className="px-4 py-3"
onClick={(e) => e.stopPropagation()}
>
{event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => onApprove?.(e, event.id)}
className="bg-green-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-green-600 transition-colors flex items-center gap-1 whitespace-nowrap"
title="Aprovar evento"
>
<CheckCircle size={12} />
Aprovar
</button>
)}
</td>
)}
</tr>
))}
</tbody>
</table>
</div>
{sortedEvents.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>Nenhum evento encontrado.</p>
</div>
)}
</div>
);
};