- Adiciona coluna de Cidade com ordenação independente na tabela de eventos - Implementa ordenação clicável em todas as colunas (nome, tipo, data, horário, cidade, local, status) - Remove dependência de estado para filtro de cidade - Adiciona exibição de curso/turma no modal de detalhes do evento - Torna campo curso/turma obrigatório no formulário de solicitação de evento - Remove campo de imagem de capa do formulário - Corrige importação de getActiveCoursesByInstitutionId no Dashboard
305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
import React, { useState, useMemo } from 'react';
|
|
import { EventData, EventStatus, UserRole } from '../types';
|
|
import { Calendar, MapPin, Users, CheckCircle, Clock, 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 = 'name' | 'type' | 'date' | 'time' | 'city' | 'location' | '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 'name':
|
|
aValue = a.name.toLowerCase();
|
|
bValue = b.name.toLowerCase();
|
|
break;
|
|
case 'type':
|
|
aValue = a.type.toLowerCase();
|
|
bValue = b.type.toLowerCase();
|
|
break;
|
|
case 'date':
|
|
aValue = new Date(a.date + 'T00:00:00').getTime();
|
|
bValue = new Date(b.date + 'T00:00:00').getTime();
|
|
break;
|
|
case 'time':
|
|
aValue = a.time;
|
|
bValue = b.time;
|
|
break;
|
|
case 'city':
|
|
aValue = a.address.city.toLowerCase();
|
|
bValue = b.address.city.toLowerCase();
|
|
break;
|
|
case 'location':
|
|
aValue = `${a.address.city}, ${a.address.state}`.toLowerCase();
|
|
bValue = `${b.address.city}, ${b.address.state}`.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.COMPLETED]: 'Concluído',
|
|
[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>
|
|
{canApprove && (
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-20">
|
|
Ações
|
|
</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('name')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
Nome do Evento
|
|
{getSortIcon('name')}
|
|
</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
|
|
{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('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('time')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
Horário
|
|
{getSortIcon('time')}
|
|
</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('city')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
Cidade
|
|
{getSortIcon('city')}
|
|
</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('location')}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
Local
|
|
{getSortIcon('location')}
|
|
</div>
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Contatos
|
|
</th>
|
|
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
|
|
Equipe
|
|
</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>
|
|
</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"
|
|
>
|
|
{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>
|
|
)}
|
|
<td className="px-4 py-3">
|
|
<div className="font-medium text-gray-900 text-sm">{event.name}</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className="text-xs text-gray-600 uppercase tracking-wide">{event.type}</span>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<Calendar size={14} className="mr-1.5 text-brand-gold flex-shrink-0" />
|
|
{formatDate(event.date)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<Clock size={14} className="mr-1.5 text-gray-400 flex-shrink-0" />
|
|
{event.time}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="text-sm text-gray-700">
|
|
{event.address.city}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<MapPin size={14} className="mr-1.5 text-brand-gold flex-shrink-0" />
|
|
<span className="truncate max-w-[200px]" title={`${event.address.street}, ${event.address.number} - ${event.address.city}/${event.address.state}`}>
|
|
{event.address.city}, {event.address.state}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center text-sm text-gray-700">
|
|
<Users size={14} className="mr-1.5 text-gray-400 flex-shrink-0" />
|
|
{event.contacts.length}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
{event.photographerIds.length > 0 ? (
|
|
<div className="flex -space-x-1">
|
|
{event.photographerIds.slice(0, 3).map((id, idx) => (
|
|
<div
|
|
key={id}
|
|
className="w-6 h-6 rounded-full border-2 border-white bg-gray-300"
|
|
style={{
|
|
backgroundImage: `url(https://i.pravatar.cc/100?u=${id})`,
|
|
backgroundSize: 'cover',
|
|
}}
|
|
title={id}
|
|
/>
|
|
))}
|
|
{event.photographerIds.length > 3 && (
|
|
<div className="w-6 h-6 rounded-full border-2 border-white bg-gray-100 flex items-center justify-center text-[10px] text-gray-600 font-medium">
|
|
+{event.photographerIds.length - 3}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<span className="text-xs text-gray-400">-</span>
|
|
)}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<span className={`inline-flex items-center px-2 py-1 rounded text-xs font-semibold ${STATUS_COLORS[event.status]}`}>
|
|
{getStatusDisplay(event.status)}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{sortedEvents.length === 0 && (
|
|
<div className="text-center py-12 text-gray-500">
|
|
<p>Nenhum evento encontrado.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|