diff --git a/frontend/pages/Dashboard.tsx b/frontend/pages/Dashboard.tsx index b6e238f..42e3ddf 100644 --- a/frontend/pages/Dashboard.tsx +++ b/frontend/pages/Dashboard.tsx @@ -211,12 +211,39 @@ export const Dashboard: React.FC = ({ setTeamAvailabilityFilter("all"); }; + const [processingIds, setProcessingIds] = useState>(new Set()); + const [teamPage, setTeamPage] = useState(1); + const ITEMS_PER_PAGE = 20; + + // Optimização 1: Pre-calcular status de ocupação (remove complexidade O(N*M) de dentro do loop) + const busyProfessionalIds = useMemo(() => { + if (!selectedEvent) return new Set(); + const busySet = new Set(); + + // Itera eventos apenas UMA vez + for (const e of events) { + if (e.id !== selectedEvent.id && e.date === selectedEvent.date) { + if (e.assignments) { + for (const a of e.assignments) { + if (a.status === 'ACEITO') { + busySet.add(a.professionalId); + } + } + } + } + } + return busySet; + }, [events, selectedEvent]); + // Função para filtrar profissionais no modal de equipe - const getFilteredTeamProfessionals = () => { + const filteredTeamProfessionals = useMemo(() => { if (!selectedEvent) return professionals; + // Reset page when filters change + setTeamPage(1); + return professionals.filter((professional) => { - // Filter out professionals with unknown roles (matches Team.tsx logic) + // Filter out professionals with unknown roles if (functions.length > 0) { const isValidRole = functions.some(f => f.id === professional.funcao_profissional_id); if (!isValidRole) return false; @@ -233,63 +260,57 @@ export const Dashboard: React.FC = ({ // Filtro por função/role if (teamRoleFilter !== "all") { const professionalFunctions = professional.functions || []; - - // Check if professional has the selected function ID in their list const hasMatchingFunction = professionalFunctions.some(f => f.id === teamRoleFilter); - - // Also check if professional.funcao_profissional_id matches (primary role) const hasMatchingPrimaryRole = professional.funcao_profissional_id === teamRoleFilter; - if (!hasMatchingFunction && !hasMatchingPrimaryRole) return false; } - // Verificar status do assignment para este evento + // Verificar assignment no evento atual + // Otimização: Evitar criar array intermediário com filter/find se possível, mas aqui é pequeno (assignments do evento) const assignment = (selectedEvent.assignments || []).find( (a) => a.professionalId === professional.id ); const status = assignment ? assignment.status : null; const isAssigned = !!status && status !== "REJEITADO"; - // Verificar se está ocupado em outro evento na mesma data - const isBusy = !isAssigned && events.some(e => - e.id !== selectedEvent.id && - e.date === selectedEvent.date && - (e.assignments || []).some(a => a.professionalId === professional.id && a.status === 'ACEITO') - ); + // Otimização 2: Usar o Set pré-calculado (O(1)) + const isBusy = !isAssigned && busyProfessionalIds.has(professional.id); // Filtro por status if (teamStatusFilter !== "all") { switch (teamStatusFilter) { - case "assigned": - if (!isAssigned) return false; - break; - case "available": - if (isAssigned || isBusy) return false; - break; - case "busy": - if (!isBusy) return false; - break; - case "rejected": - if (!status || status !== "REJEITADO") return false; - break; + case "assigned": if (!isAssigned) return false; break; + case "available": if (isAssigned || isBusy) return false; break; + case "busy": if (!isBusy) return false; break; + case "rejected": if (!status || status !== "REJEITADO") return false; break; } } // Filtro por disponibilidade if (teamAvailabilityFilter !== "all") { switch (teamAvailabilityFilter) { - case "available": - if (isAssigned || isBusy) return false; - break; - case "unavailable": - if (!isAssigned && !isBusy) return false; - break; + case "available": if (isAssigned || isBusy) return false; break; + case "unavailable": if (!isAssigned && !isBusy) return false; break; } } return true; }); - }; + }, [ + selectedEvent, + professionals, + functions, + teamSearchTerm, + teamRoleFilter, + teamStatusFilter, + teamAvailabilityFilter, + busyProfessionalIds // Depende apenas do Set otimizado + ]); + + // Paginação + const displayedTeamProfessionals = useMemo(() => { + return filteredTeamProfessionals.slice(0, teamPage * ITEMS_PER_PAGE); + }, [filteredTeamProfessionals, teamPage]); // Guard Clause for basic security if (!user) @@ -473,8 +494,10 @@ export const Dashboard: React.FC = ({ setIsTeamModalOpen(true); }; - const togglePhotographer = (photographerId: string) => { + const togglePhotographer = async (photographerId: string) => { if (!selectedEvent) return; + if (processingIds.has(photographerId)) return; // Prevent double click + const prof = professionals.find(p => p.id === photographerId); const assignment = selectedEvent.assignments?.find(a => a.professionalId === photographerId); @@ -483,13 +506,31 @@ export const Dashboard: React.FC = ({ return; } - assignPhotographer(selectedEvent.id, photographerId); + setProcessingIds(prev => new Set(prev).add(photographerId)); + try { + await assignPhotographer(selectedEvent.id, photographerId); + } finally { + setProcessingIds(prev => { + const next = new Set(prev); + next.delete(photographerId); + return next; + }); + } }; - const handleRoleSelect = (funcaoId: string) => { + const handleRoleSelect = async (funcaoId: string) => { if (roleSelectionProf && selectedEvent) { - assignPhotographer(selectedEvent.id, roleSelectionProf.id, funcaoId); - setRoleSelectionProf(null); + setProcessingIds(prev => new Set(prev).add(roleSelectionProf.id)); + try { + await assignPhotographer(selectedEvent.id, roleSelectionProf.id, funcaoId); + } finally { + setProcessingIds(prev => { + const next = new Set(prev); + next.delete(roleSelectionProf.id); + return next; + }); + setRoleSelectionProf(null); + } } }; @@ -1461,21 +1502,31 @@ export const Dashboard: React.FC = ({ - {getFilteredTeamProfessionals().map((photographer) => { + {displayedTeamProfessionals.map((photographer) => { const assignment = (selectedEvent.assignments || []).find( (a) => a.professionalId === photographer.id ); - const status = assignment ? assignment.status : null; const isAssigned = !!status && status !== "REJEITADO"; - // Check if busy in other events on the same date - const busyEvent = !isAssigned ? events.find(e => + // Optimization: Use busySet (O(1)) + // We need the busy EVENT details if we want to show WHERE they are busy. + // But for speed, just checking if busy is enough for filtering. + // To show tooltip, we can do a lookup ONLY if they are busy. + + // Check if busy in other events on the same date (using logic or Set) + const isBusyInSet = busyProfessionalIds.has(photographer.id); + const isBusy = !isAssigned && isBusyInSet; + + // Only search for the specific event if we need to show the tooltip and they ARE busy + // This avoids searching for everyone. + const busyEvent = isBusy ? events.find(e => e.id !== selectedEvent.id && e.date === selectedEvent.date && (e.assignments || []).some(a => a.professionalId === photographer.id && a.status === 'ACEITO') ) : undefined; - const isBusy = !!busyEvent; + + const isProcessing = processingIds.has(photographer.id); return ( = ({ onClick={() => togglePhotographer(photographer.id) } - disabled={!status && isBusy} - className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${status === "ACEITO" || status === "PENDENTE" + disabled={(!status && isBusy) || isProcessing} + className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors flex items-center justify-center gap-2 ${status === "ACEITO" || status === "PENDENTE" ? "bg-red-100 text-red-700 hover:bg-red-200" : "bg-brand-gold text-white hover:bg-[#a5bd2e]" - }`} + } ${isProcessing ? "opacity-70 cursor-wait" : ""}`} > + {isProcessing &&
} {status === "ACEITO" || status === "PENDENTE" ? "Remover" : "Adicionar"} ); })} - {getFilteredTeamProfessionals().length === 0 && ( + + {/* Empty State */} + {filteredTeamProfessionals.length === 0 && ( -
+ {/* ... existing empty state ... */} +

- {professionals.length === 0 - ? "Nenhum profissional disponível para esta data" - : "Nenhum profissional encontrado com os filtros aplicados" - } + Ninguém encontrado.

-

- {professionals.length === 0 - ? "Tente selecionar outra data ou entre em contato com a equipe" - : "Tente ajustar os filtros de busca" - } -

-
+
+ + + )} + + {/* Load More Row */} + {displayedTeamProfessionals.length < filteredTeamProfessionals.length && ( + + + )} @@ -1596,18 +1656,16 @@ export const Dashboard: React.FC = ({ {/* Lista de Cards (Mobile) */}
- {getFilteredTeamProfessionals().map((photographer) => { + {displayedTeamProfessionals.map((photographer) => { const assignment = (selectedEvent.assignments || []).find( (a) => a.professionalId === photographer.id ); const status = assignment ? assignment.status : null; + const isAssigned = !!status && status !== "REJEITADO"; - // Check if busy in other events on the same date - const isBusy = !status && events.some(e => - e.id !== selectedEvent.id && - e.date === selectedEvent.date && - e.photographerIds.includes(photographer.id) - ); + const isBusyInSet = busyProfessionalIds.has(photographer.id); + const isBusy = !isAssigned && isBusyInSet; + const isProcessing = processingIds.has(photographer.id); return (
@@ -1667,19 +1725,32 @@ export const Dashboard: React.FC = ({
); })} - {professionals.length === 0 && ( + + {/* Load More Mobile */} + {displayedTeamProfessionals.length < filteredTeamProfessionals.length && ( +
+ +
+ )} + {filteredTeamProfessionals.length === 0 && (

Nenhum profissional disponível.