photum/frontend/components/EventTable.tsx
NANDO9322 9906db8bc6 feat: destaca eventos de pré-venda e adiciona filtros de status da turma
- Destaque em azul para pré-venda na grid.
- Filtros por status (Pré-venda/Finalizada) no Dashboard.
- Badges de status nos detalhes do evento.
- Ajustes no backend para expor campo `pre_venda` da FOT.
2026-02-10 16:36:57 -03:00

873 lines
39 KiB
TypeScript

import React, { useState, useMemo } from "react";
import { EventData, EventStatus, UserRole } from "../types";
import { CheckCircle, ArrowUpDown, ArrowUp, ArrowDown, AlertCircle } from "lucide-react";
import { STATUS_COLORS } from "../constants";
interface EventTableProps {
events: EventData[];
onEventClick: (event: EventData) => void;
onApprove?: (e: React.MouseEvent, eventId: string) => void;
onReject?: (e: React.MouseEvent, eventId: string, reason?: string) => void;
userRole: UserRole;
currentProfessionalId?: string;
onAssignmentResponse?: (e: React.MouseEvent, eventId: string, status: string, reason?: string) => void;
isManagingTeam?: boolean; // Nova prop para determinar se está na tela de gerenciar equipe
professionals?: any[]; // Lista de profissionais para cálculos de gestão de equipe
functions?: any[]; // Lista de funções disponíveis
isLoading?: boolean;
onFinalizeClass?: (e: React.MouseEvent, event: EventData) => void;
}
type SortField =
| "fot"
| "date"
| "curso"
| "instituicao"
| "anoFormatura"
| "empresa"
| "type"
| "status";
type SortOrder = "asc" | "desc" | null;
export const EventTable: React.FC<EventTableProps> = ({
events,
onEventClick,
onApprove,
onReject,
userRole,
currentProfessionalId,
onAssignmentResponse,
isManagingTeam = false,
professionals = [],
functions = [],
isLoading = false,
onFinalizeClass,
}) => {
const canApprove = isManagingTeam && (userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN);
const canReject = userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN;
const isPhotographer = userRole === UserRole.PHOTOGRAPHER;
// Função para calcular status da equipe
const calculateTeamStatus = (event: EventData) => {
const assignments = event.assignments || [];
// Helper to check if assignment handles a specific role
const isAssignedToRole = (assignment: any, roleSlug: string) => {
// If assignment has a specific function ID, check against that function's name
if (assignment.funcaoId && professionals && professionals.length > 0) {
// Find the function definition in the professionals list or a separate functions list?
// The plan said pass `functions` list.
// But checking `assignment.funcaoId` against `functions` list is cleaner.
// Let's assume we receive `functions` prop.
if (functions) {
const func = functions.find((f:any) => f.id === assignment.funcaoId);
if (func) {
return func.nome.toLowerCase().includes(roleSlug.toLowerCase());
}
}
}
// Fallback for legacy data or if assignment.funcaoId is missing (backward compatibility)
// Check the professional's capability (OLD BEHAVIOR - CAUSES DOUBLE COUNT if multi-role)
// ONLY fallback if funcaoId is missing.
if (!assignment.funcaoId) {
const professional = professionals.find(p => p.id === assignment.professionalId);
if (!professional) return false;
// Check functions array first
if (professional.functions && professional.functions.length > 0) {
return professional.functions.some((f: any) => f.nome.toLowerCase().includes(roleSlug.toLowerCase()));
}
return (professional.role || "").toLowerCase().includes(roleSlug.toLowerCase());
}
return false;
};
// Contadores de profissionais aceitos por tipo
const acceptedFotografos = assignments.filter(a =>
a.status === "ACEITO" && isAssignedToRole(a, "fot")
).length;
const acceptedRecepcionistas = assignments.filter(a =>
a.status === "ACEITO" && isAssignedToRole(a, "recep")
).length;
const acceptedCinegrafistas = assignments.filter(a =>
a.status === "ACEITO" && isAssignedToRole(a, "cine")
).length;
// Quantidades necessárias
const qtdFotografos = event.qtdFotografos || 0;
const qtdRecepcionistas = event.qtdRecepcionistas || 0;
const qtdCinegrafistas = event.qtdCinegrafistas || 0;
// Calcular faltantes
const fotoFaltante = Math.max(0, qtdFotografos - acceptedFotografos);
const recepFaltante = Math.max(0, qtdRecepcionistas - acceptedRecepcionistas);
const cineFaltante = Math.max(0, qtdCinegrafistas - acceptedCinegrafistas);
// Verificar se todos os profissionais estão OK
const profissionaisOK = fotoFaltante === 0 && recepFaltante === 0 && cineFaltante === 0;
return {
acceptedFotografos,
acceptedRecepcionistas,
acceptedCinegrafistas,
fotoFaltante,
recepFaltante,
cineFaltante,
profissionaisOK
};
};
const [sortField, setSortField] = useState<SortField | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
const handleSort = (field: SortField) => {
if (sortField === field) {
if (sortOrder === "asc") {
setSortOrder("desc");
} else if (sortOrder === "desc") {
setSortOrder(null);
setSortField(null);
}
} else {
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":
// Parse date robustly
const dateA = a.date ? new Date(a.date.includes('T') ? a.date : a.date + "T00:00:00") : new Date(0);
const dateB = b.date ? new Date(b.date.includes('T') ? b.date : b.date + "T00:00:00") : new Date(0);
aValue = !isNaN(dateA.getTime()) ? dateA.getTime() : 0;
bValue = !isNaN(dateB.getTime()) ? dateB.getTime() : 0;
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]);
// Pagination State
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 50;
const totalPages = Math.ceil(sortedEvents.length / itemsPerPage);
const paginatedEvents = useMemo(() => {
const start = (currentPage - 1) * itemsPerPage;
return sortedEvents.slice(start, start + itemsPerPage);
}, [sortedEvents, currentPage]);
// Reset page when filters/sort change (implicitly when sortedEvents length changes drastically, though here sortedEvents updates)
React.useEffect(() => {
setCurrentPage(1);
}, [events.length, sortField, sortOrder]); // Reset if data source changes significantly
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;
};
// Scroll Sync Logic
const tableContainerRef = React.useRef<HTMLDivElement>(null);
const topScrollRef = React.useRef<HTMLDivElement>(null);
const [tableScrollWidth, setTableScrollWidth] = useState(0);
// Sync scroll width
React.useEffect(() => {
if (tableContainerRef.current) {
setTableScrollWidth(tableContainerRef.current.scrollWidth);
}
}, [events, sortedEvents, isManagingTeam]); // Dependencies that change table width
const handleTopScroll = () => {
if (topScrollRef.current && tableContainerRef.current) {
tableContainerRef.current.scrollLeft = topScrollRef.current.scrollLeft;
}
};
const handleTableScroll = () => {
if (topScrollRef.current && tableContainerRef.current) {
topScrollRef.current.scrollLeft = tableContainerRef.current.scrollLeft;
}
};
return (
<div className="bg-white rounded-lg border border-gray-200 overflow-hidden shadow-sm flex flex-col">
{/* Mobile Card View */}
<div className="md:hidden divide-y divide-gray-100">
{sortedEvents.map((event) => {
let photographerAssignment = null;
if (isPhotographer && currentProfessionalId && event.assignments) {
photographerAssignment = event.assignments.find(a => a.professionalId === currentProfessionalId);
}
return (
<div
key={event.id}
className="p-4 hover:bg-gray-50 active:bg-gray-100 transition-colors cursor-pointer"
onClick={() => onEventClick(event)}
>
<div className="flex justify-between items-start mb-2">
<div>
<span className="font-bold text-gray-900 block">FOT {event.fot || "-"}</span>
<span className="text-xs text-gray-500">{event.type}</span>
</div>
<span className="text-sm font-medium text-gray-600 bg-gray-100 px-2 py-1 rounded">
{formatDate(event.date)}
</span>
</div>
<div className="mb-3 space-y-1">
<p className="text-sm text-gray-800 font-medium">{event.curso || "Sem curso defined"}</p>
<p className="text-sm text-gray-600">{event.instituicao || "-"}</p>
<div className="flex items-center gap-2 text-xs text-gray-500">
<span>{event.empresa || "-"}</span>
<span></span>
<span>{event.anoFormatura || "-"}</span>
</div>
</div>
<div className="flex justify-between items-center mt-3 pt-3 border-t border-gray-50">
<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>
{(canApprove || canReject || isPhotographer) && (
<div onClick={(e) => e.stopPropagation()} className="flex items-center gap-2">
{canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => onApprove?.(e, event.id)}
className="bg-green-500 text-white p-2 rounded-full hover:bg-green-600 transition-colors shadow-sm"
title="Aprovar evento"
>
<CheckCircle size={16} />
</button>
)}
{canReject && !canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição (opcional):");
onReject?.(e, event.id, reason || undefined);
}}
className="bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors shadow-sm"
title="Recusar evento"
>
</button>
)}
{isPhotographer && photographerAssignment && (
<>
{photographerAssignment.status === "PENDENTE" && (
<div className="flex gap-2">
<button
onClick={(e) => onAssignmentResponse?.(e, event.id, "ACEITO")}
className="bg-green-100 text-green-700 px-3 py-1 rounded-md text-xs font-bold border border-green-200"
>
Aceitar
</button>
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição (opcional):");
onAssignmentResponse?.(e, event.id, "REJEITADO", reason || undefined);
}}
className="bg-red-100 text-red-700 px-3 py-1 rounded-md text-xs font-bold border border-red-200"
>
Rejeitar
</button>
</div>
)}
{photographerAssignment.status === "ACEITO" && (
<span className="text-green-600 text-xs font-bold border border-green-200 bg-green-50 px-2 py-1 rounded">Aceito</span>
)}
{photographerAssignment.status === "REJEITADO" && (
<span className="text-red-600 text-xs font-bold border border-red-200 bg-red-50 px-2 py-1 rounded">Rejeitado</span>
)}
</>
)}
</div>
)}
</div>
</div>
);
})}
</div>
<div className="hidden md:block overflow-x-auto border-b border-gray-200 bg-gray-50 mb-1"
ref={topScrollRef}
onScroll={handleTopScroll}
style={{ minHeight: '12px' }}
>
<div style={{ width: tableScrollWidth > 0 ? tableScrollWidth : '100%', height: '1px' }}></div>
</div>
<div className="hidden md:block">
<PaginationControls
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
</div>
<div
className="hidden md:block overflow-x-auto"
ref={tableContainerRef}
onScroll={handleTableScroll}
>
<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>
{/* Novas colunas de gestão de equipe */}
{(userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN) && (
<>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Quantidade de Formandos">
QTD Form.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Quantidade de Fotógrafos">
Fotóg.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Quantidade de Recepcionistas">
Recep.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Quantidade de Cinegrafistas">
Cine.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Quantidade de Estúdios">
Estúd.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Pontos de Foto">
Pts. Foto
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Pontos Decorados">
Pts. Dec.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Pontos de LED">
Pts. LED
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Profissionais Confirmados?">
Prof. OK?
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Fotógrafos Faltantes/Pendentes">
Fot. Falt.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Recepcionistas Faltantes/Pendentes">
Rec. Falt.
</th>
<th className="px-3 py-3 text-center text-xs font-semibold text-gray-600 uppercase tracking-wider" title="Cinegrafistas Faltantes/Pendentes">
Cin. Falt.
</th>
</>
)}
{(canApprove || isPhotographer) && (
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider w-32">
Ações
</th>
)}
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{isLoading ? (
<tr>
<td colSpan={12} className="py-20 text-center">
<div className="flex flex-col items-center justify-center">
<div className="w-10 h-10 border-4 border-gray-100 rounded-full border-t-brand-gold animate-spin mb-4"></div>
<p className="text-gray-500 font-medium">Carregando dados...</p>
</div>
</td>
</tr>
) : (
paginatedEvents.map((event) => {
// Logic to find photographer assignment status
let photographerAssignment = null;
if (isPhotographer && currentProfessionalId && event.assignments) {
photographerAssignment = event.assignments.find(a => a.professionalId === currentProfessionalId);
}
return (
<tr
key={event.id}
onClick={() => onEventClick(event)}
className={`cursor-pointer transition-colors border-l-4 ${
event.fot_finalizada
? "bg-red-50 hover:bg-red-100 border-l-red-500"
: ((event.fot_pre_venda || event.pre_venda) && (userRole === UserRole.SUPERADMIN || userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.RESEARCHER))
? "bg-blue-50 hover:bg-blue-100 border-l-blue-500"
: "border-l-transparent hover:bg-gray-50"
}`}
>
<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>
{/* Novas células de gestão de equipe */}
{(userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN) && (() => {
const teamStatus = calculateTeamStatus(event);
return (
<>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdFormandos || event.attendees || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdFotografos || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdRecepcionistas || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdCinegrafistas || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdEstudios || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdPontosFoto || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdPontosDecorados || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className="text-xs text-gray-900">
{event.qtdPontosLed || "-"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
teamStatus.profissionaisOK
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}`}>
{teamStatus.profissionaisOK ? "✓" : "✗"}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`text-xs font-medium ${
teamStatus.fotoFaltante > 0 ? "text-red-600" : "text-green-600"
}`}>
{teamStatus.fotoFaltante}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`text-xs font-medium ${
teamStatus.recepFaltante > 0 ? "text-red-600" : "text-green-600"
}`}>
{teamStatus.recepFaltante}
</span>
</td>
<td className="px-3 py-3 text-center">
<span className={`text-xs font-medium ${
teamStatus.cineFaltante > 0 ? "text-red-600" : "text-green-600"
}`}>
{teamStatus.cineFaltante}
</span>
</td>
</>
);
})()}
{(canApprove || canReject || isPhotographer) && (
<td
className="px-4 py-3"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-2">
{canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<div className="flex flex-col gap-1">
<button
onClick={(e) => {
onApprove?.(e, event.id);
}}
className="px-2 py-1 rounded text-xs font-semibold flex items-center gap-1 whitespace-nowrap transition-colors bg-green-500 text-white hover:bg-green-600"
title="Aprovar evento"
>
<CheckCircle size={12} />
Aprovar
</button>
</div>
)}
{canReject && !canApprove && event.status === EventStatus.PENDING_APPROVAL && (
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição (opcional):");
onReject?.(e, event.id, reason || undefined);
}}
className="bg-red-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-red-600 transition-colors flex items-center gap-1 whitespace-nowrap"
title="Recusar evento"
>
Recusar
</button>
)}
{isPhotographer && photographerAssignment && (
<>
{photographerAssignment.status === "PENDENTE" && (
<>
<button
onClick={(e) => onAssignmentResponse?.(e, event.id, "ACEITO")}
className="bg-green-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-green-600 transition-colors"
title="Aceitar"
>
Aceitar
</button>
<button
onClick={(e) => {
const reason = prompt("Motivo da rejeição (opcional):");
onAssignmentResponse?.(e, event.id, "REJEITADO", reason || undefined);
}}
className="bg-red-500 text-white px-2 py-1 rounded text-xs font-semibold hover:bg-red-600 transition-colors"
title="Rejeitar"
>
Rejeitar
</button>
</>
)}
{photographerAssignment.status === "ACEITO" && (
<span className="text-green-600 text-xs font-bold border border-green-200 bg-green-50 px-2 py-1 rounded">Aceito</span>
)}
{photographerAssignment.status === "REJEITADO" && (
<div className="flex items-center gap-2">
<span className="text-red-600 text-xs font-bold border border-red-200 bg-red-50 px-2 py-1 rounded">Rejeitado</span>
{photographerAssignment.reason && (
<button
onClick={(e) => {
e.stopPropagation();
alert(`Motivo: ${photographerAssignment.reason}`);
}}
className="bg-gray-100 text-gray-600 px-2 py-1 rounded text-xs hover:bg-gray-200 transition-colors"
title="Ver motivo"
>
Motivo
</button>
)}
</div>
)}
</>
)}
</div>
<div className="mt-1">
{(userRole === UserRole.BUSINESS_OWNER || userRole === UserRole.SUPERADMIN) && onFinalizeClass && (
<button
onClick={(e) => {
e.stopPropagation();
onFinalizeClass(e, event);
}}
className={`px-2 py-1 rounded text-xs font-semibold flex items-center gap-1 whitespace-nowrap transition-colors border ${event.fot_finalizada ? "bg-green-100 text-green-700 border-green-200" : "bg-red-50 text-red-700 border-red-200"}`}
title={event.fot_finalizada ? "Reabrir Turma" : "Finalizar Turma"}
>
<AlertCircle size={12} />
{event.fot_finalizada ? "Reabrir" : "Finalizar"}
</button>
)}
</div>
</td>
)}
</tr>
);
})
)}
</tbody>
</table>
</div>
{/* Pagination Controls */}
<PaginationControls
currentPage={currentPage}
totalPages={totalPages}
onPageChange={setCurrentPage}
/>
{sortedEvents.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>Nenhum evento encontrado.</p>
</div>
)}
</div>
);
};
interface PaginationControlsProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
const PaginationControls: React.FC<PaginationControlsProps> = ({ currentPage, totalPages, onPageChange }) => {
if (totalPages <= 1) return null;
return (
<div className="flex items-center justify-between px-4 py-3 bg-white border-t border-b border-gray-200 sm:px-6">
<div className="flex justify-between w-full sm:hidden">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50"
>
Anterior
</button>
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="relative ml-3 inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50"
>
Próxima
</button>
</div>
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Mostrando página <span className="font-medium">{currentPage}</span> de{" "}
<span className="font-medium">{totalPages}</span>
</p>
</div>
<div>
<nav className="inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
<button
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
>
<span className="sr-only">Anterior</span>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clipRule="evenodd" />
</svg>
</button>
{/* Simplified Pagination Numbers */}
{[...Array(Math.min(5, totalPages))].map((_, idx) => {
// Logic to show pages around current
let pageNum = currentPage;
if (totalPages <= 5) pageNum = idx + 1;
else if (currentPage <= 3) pageNum = idx + 1;
else if (currentPage >= totalPages - 2) pageNum = totalPages - 4 + idx;
else pageNum = currentPage - 2 + idx;
return (
<button
key={pageNum}
onClick={() => onPageChange(pageNum)}
className={`relative inline-flex items-center px-4 py-2 text-sm font-semibold ${currentPage === pageNum ? 'bg-brand-gold text-white focus:z-20 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-gold' : 'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0'}`}
>
{pageNum}
</button>
)
})}
<button
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
>
<span className="sr-only">Próxima</span>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clipRule="evenodd" />
</svg>
</button>
</nav>
</div>
</div>
</div>
);
};