import React, { useState, useEffect, useMemo } from "react"; import { useNavigate } from 'react-router-dom'; import { UserRole, EventData, EventStatus, EventType, Professional } from "../types"; import { EventTable } from "../components/EventTable"; import { EventFiltersBar, EventFilters } from "../components/EventFiltersBar"; import { EventForm } from "../components/EventForm"; import { Button } from "../components/Button"; import { PlusCircle, Search, CheckCircle, Clock, Edit, Users, Map, Building2, Calendar, MapPin, X, UserCheck, UserX, AlertCircle, Star, } from "lucide-react"; import { setCoordinator, finalizeFOT, getPrice } from "../services/apiService"; import { useAuth } from "../contexts/AuthContext"; import { useData } from "../contexts/DataContext"; import { STATUS_COLORS } from "../constants"; import { ProfessionalDetailsModal } from "../components/ProfessionalDetailsModal"; interface DashboardProps { initialView?: "list" | "create"; } export const Dashboard: React.FC = ({ initialView = "list", }) => { const { user, token } = useAuth(); const navigate = useNavigate(); // Extract updateEventDetails from useData const { events, getEventsByRole, addEvent, updateEventStatus, assignPhotographer, professionals, getInstitutionById, getActiveCoursesByInstitutionId, respondToAssignment, updateEventDetails, functions, isLoading, refreshEvents, } = useData(); // ... (inside component) const handleSaveEvent = async (data: any) => { const isClient = user.role === UserRole.EVENT_OWNER; if (view === "edit" && selectedEvent) { if (updateEventDetails) { await updateEventDetails(selectedEvent.id, data); // Force reload of view to reflect changes (or rely on DataContext optimistic update) // But DataContext optimistic update only touched generic fields. // Address might still be old in 'selectedEvent' state if we don't update it. // Updating selectedEvent manually as well to be safe, mapping snake_case payload to camelCase state: const updatedEvent = { ...selectedEvent, ...data, date: data.date || data.data_evento?.split('T')[0] || selectedEvent.date, // Map snake_case fields used in EventForm payload to camelCase fields used in UI qtdFormandos: data.qtd_formandos ?? selectedEvent.qtdFormandos, qtdFotografos: data.qtd_fotografos ?? selectedEvent.qtdFotografos, qtdRecepcionistas: data.qtd_recepcionistas ?? selectedEvent.qtdRecepcionistas, qtdCinegrafistas: data.qtd_cinegrafistas ?? selectedEvent.qtdCinegrafistas, qtdEstudios: data.qtd_estudios ?? selectedEvent.qtdEstudios, qtdPontosFoto: data.qtd_pontos_foto ?? selectedEvent.qtdPontosFoto, qtdPontosDecorados: data.qtd_pontos_decorados ?? selectedEvent.qtdPontosDecorados, qtdPontosLed: data.qtd_pontos_led ?? selectedEvent.qtdPontosLed, qtdPlataforma360: data.qtd_plataforma_360 ?? selectedEvent.qtdPlataforma360, name: data.observacoes_evento || selectedEvent.name, briefing: data.observacoes_evento || selectedEvent.briefing, time: data.horario || selectedEvent.time, }; setSelectedEvent(updatedEvent); setView("details"); // Reloading window to ensure total consistency with backend as fallback // window.location.reload(); // Commented out to try SPA update first } else { console.error("Update function not available"); } } else { const initialStatus = isClient ? EventStatus.PENDING_APPROVAL : EventStatus.PLANNING; const newEvent: EventData = { ...data, id: Math.random().toString(36).substr(2, 9), status: initialStatus, checklist: [], ownerId: isClient ? user.id : "unknown", photographerIds: [], }; await addEvent(newEvent); setView("list"); } }; const [view, setView] = useState<"list" | "create" | "edit" | "details">(() => { const params = new URLSearchParams(window.location.search); return params.get("eventId") ? "details" : initialView; }); const [searchTerm, setSearchTerm] = useState(""); const [selectedEvent, setSelectedEvent] = useState(() => { const params = new URLSearchParams(window.location.search); const eventId = params.get("eventId"); if (eventId && events.length > 0) { return events.find(e => e.id === eventId) || null; } return null; }); // Effect to sync selectedEvent if events load LATER than initial render useEffect(() => { const params = new URLSearchParams(window.location.search); const eventId = params.get("eventId"); if (eventId && events.length > 0 && !selectedEvent) { const found = events.find(e => e.id === eventId); if (found) { setSelectedEvent(found); // Ensure view is details if we just found it setView("details"); } } }, [events, window.location.search]); const [activeFilter, setActiveFilter] = useState("all"); const [advancedFilters, setAdvancedFilters] = useState({ date: "", fotId: "", type: "", company: "", institution: "", }); const [isTeamModalOpen, setIsTeamModalOpen] = useState(false); const [viewingProfessional, setViewingProfessional] = useState(null); // Estados para filtros do modal de equipe const [teamSearchTerm, setTeamSearchTerm] = useState(""); const [teamRoleFilter, setTeamRoleFilter] = useState("all"); const [teamStatusFilter, setTeamStatusFilter] = useState("all"); const [teamAvailabilityFilter, setTeamAvailabilityFilter] = useState("all"); const [roleSelectionProf, setRoleSelectionProf] = useState(null); const [basePrice, setBasePrice] = useState(null); useEffect(() => { const fetchBasePrice = async () => { if (!selectedEvent || !user || !token) { setBasePrice(null); return; } // Only for professionals (skip owners/admins unless they want to see it? User asked for professional view) // If user is admin but viewing as professional context? No, user req specifically: "ao convidar um profissional... no painel dele" if (user.role === UserRole.EVENT_OWNER || user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) { setBasePrice(null); return; } const currentProf = professionals.find(p => p.usuarioId === user.id); if (!currentProf) return; // Determine Service Name let serviceName = ""; // 1. Check assignment const assignment = selectedEvent.assignments?.find(a => a.professionalId === currentProf.id); if (assignment && assignment.funcaoId && functions) { const fn = functions.find(f => f.id === assignment.funcaoId); if (fn) serviceName = fn.nome; } // 2. Fallback to professional functions/role if (!serviceName) { if (currentProf.functions && currentProf.functions.length > 0) { // Use first function as default base? Or try to match? // Simple approach: use first. serviceName = currentProf.functions[0].nome; } else { serviceName = currentProf.role; } } // Map legacy roles if needed (Backend expects names matching tipos_servicos) // e.g. "fotografo" -> "Fotografia"? Check backend migration/seeds. // Assuming names match or backend handles fuzzy match. // Backend GetStandardPrice uses ILIKE on tipos_servicos.nome. // "Fotógrafo" might need to map to "Fotografia". // "Cinegrafista" -> "Cinegrafia". // Let's try raw first, but maybe normalize if needed. // Actually, let's map common ones just in case. // Map legacy roles and English enums const serviceLower = serviceName.toLowerCase(); if (serviceLower.includes("fot") || serviceLower === "photographer") serviceName = "Fotógrafo"; else if (serviceLower.includes("cine") || serviceLower === "videographer") serviceName = "Cinegrafista"; else if (serviceLower.includes("recep")) serviceName = "Recepcionista"; else if (serviceLower === "drone") serviceName = "Drone"; // Example console.log("Fetch Base Price Debug:", { currentProfId: currentProf.id, assignmentFound: !!assignment, rawService: serviceLower, finalService: serviceName, eventType: selectedEvent.type }); if (serviceName && selectedEvent.type) { try { const res = await getPrice(token, selectedEvent.type, serviceName); console.log("Fetch Base Price Result:", res); if (res.data) { setBasePrice(res.data.valor); } else { console.warn("Base Price returned no data"); setBasePrice(0); } } catch (err) { console.error("Error fetching base price:", err); setBasePrice(0); } } else { console.warn("Skipping price fetch: Missing serviceName or eventType"); } }; fetchBasePrice(); }, [selectedEvent, user, token, professionals, functions]); // ... (existing code) ... // Inside render (around line 1150 in original file, need to find the "Time" card) // Look for:
... Horário ...
useEffect(() => { if (initialView) { setView(initialView); if (initialView === "create") setSelectedEvent(null); } }, [initialView]); const handleViewProfessional = (professional: Professional) => { setViewingProfessional(professional); }; // Função para calcular profissionais faltantes e status const calculateTeamStatus = (event: EventData) => { const assignments = event.assignments || []; // Contadores de profissionais aceitos por tipo // Helper to check if professional has a specific role const hasRole = (professional: Professional | undefined, roleSlug: string, assignedFuncaoId?: string) => { if (!professional) return false; const term = roleSlug.toLowerCase(); // 1. If assignment has explicit function, check it if (assignedFuncaoId) { const fn = functions.find(f => f.id === assignedFuncaoId); if (fn && fn.nome.toLowerCase().includes(term)) return true; // If term didn't match the assigned function, return false (exclusive assignment) // Unless we fallback? No, if assigned as Cine, shouldn't count as Fot. if (fn) return false; } // 2. Fallback to existing logic (capabilities) if no function assigned // Check functions array first (new multi-role system) if (professional.functions && professional.functions.length > 0) { return professional.functions.some(f => f.nome.toLowerCase().includes(term)); } // Fallback to legacy role field return (professional.role || "").toLowerCase().includes(term); }; // Contadores de profissionais aceitos por tipo const acceptedFotografos = assignments.filter(a => a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "fot", a.funcaoId) ).length; const pendingFotografos = assignments.filter(a => a.status === "PENDENTE" && hasRole(professionals.find(p => p.id === a.professionalId), "fot", a.funcaoId) ).length; const acceptedRecepcionistas = assignments.filter(a => a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "recep", a.funcaoId) ).length; const pendingRecepcionistas = assignments.filter(a => a.status === "PENDENTE" && hasRole(professionals.find(p => p.id === a.professionalId), "recep", a.funcaoId) ).length; const acceptedCinegrafistas = assignments.filter(a => a.status === "ACEITO" && hasRole(professionals.find(p => p.id === a.professionalId), "cine", a.funcaoId) ).length; const pendingCinegrafistas = assignments.filter(a => a.status === "PENDENTE" && hasRole(professionals.find(p => p.id === a.professionalId), "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, pendingFotografos, acceptedRecepcionistas, pendingRecepcionistas, acceptedCinegrafistas, pendingCinegrafistas, fotoFaltante, recepFaltante, cineFaltante, profissionaisOK, qtdFotografos, qtdRecepcionistas, qtdCinegrafistas }; }; const handleSetCoordinator = async (professionalId: string, currentStatus: boolean) => { if (!user || !selectedEvent) return; const token = localStorage.getItem("token"); if (!token) return; const res = await setCoordinator(token, selectedEvent.id, professionalId, !currentStatus); if (res.error) { alert("Erro ao definir coordenador: " + res.error); } else { // Optimistic update setSelectedEvent(prev => prev ? ({ ...prev, assignments: prev.assignments?.map(a => a.professionalId === professionalId ? { ...a, is_coordinator: !currentStatus } : a ) }) : null); // Force refresh to get data from server and persist color setTimeout(() => { refreshEvents(); }, 500); } }; // Função para fechar modal de equipe e limpar filtros const closeTeamModal = () => { setIsTeamModalOpen(false); setTeamSearchTerm(""); setTeamRoleFilter("all"); setTeamStatusFilter("all"); 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 filteredTeamProfessionals = useMemo(() => { if (!selectedEvent) return professionals; // Reset page when filters change setTeamPage(1); return professionals.filter((professional) => { // 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; } // Filtro por busca (nome ou email) if (teamSearchTerm) { const searchLower = teamSearchTerm.toLowerCase(); const nameMatch = (professional.name || professional.nome || "").toLowerCase().includes(searchLower); const emailMatch = (professional.email || "").toLowerCase().includes(searchLower); if (!nameMatch && !emailMatch) return false; } // Filtro por função/role if (teamRoleFilter !== "all") { const professionalFunctions = professional.functions || []; const hasMatchingFunction = professionalFunctions.some(f => f.id === teamRoleFilter); const hasMatchingPrimaryRole = professional.funcao_profissional_id === teamRoleFilter; if (!hasMatchingFunction && !hasMatchingPrimaryRole) return false; } // 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"; // 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; } } // Filtro por disponibilidade if (teamAvailabilityFilter !== "all") { switch (teamAvailabilityFilter) { 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) return
Acesso Negado. Faça login.
; const myEvents = getEventsByRole(user.id, user.role); const currentProfessionalId = user.role === UserRole.PHOTOGRAPHER ? professionals.find((p) => p.usuarioId === user.id)?.id : undefined; // Extract unique values for filters const { availableTypes, availableCompanies, availableInstitutions } = useMemo(() => { const types = [...new Set(myEvents.map((e) => e.type))].sort(); const companies = [...new Set(myEvents.map((e) => e.empresa).filter(Boolean))].sort(); const institutions = [...new Set(myEvents.map((e) => e.instituicao).filter(Boolean))].sort(); return { availableTypes: types, availableCompanies: companies, availableInstitutions: institutions, }; }, [myEvents]); // Removed the previous useEffect for eventId to avoid conflicts with the new initialization logic // and separate sync effect above. /* // Check for eventId in URL on mount useEffect(() => { ... }, [events, window.location.search]); */ // Combined filtering logic const filteredEvents = myEvents.filter((e) => { const matchesSearch = e.name .toLowerCase() .includes(searchTerm.toLowerCase()); const matchesStatus = activeFilter === "all" || (activeFilter === "pending" && e.status === EventStatus.PENDING_APPROVAL) || (activeFilter === "active" && e.status !== EventStatus.ARCHIVED && e.status !== EventStatus.PENDING_APPROVAL); // Advanced filters const matchesDate = !advancedFilters.date || e.date === advancedFilters.date; const matchesFot = !advancedFilters.fotId || String(e.fot || "").toLowerCase().includes(advancedFilters.fotId.toLowerCase()); const matchesType = !advancedFilters.type || e.type === advancedFilters.type; const matchesCompany = !advancedFilters.company || e.empresa === advancedFilters.company; const matchesInstitution = !advancedFilters.institution || e.instituicao === advancedFilters.institution; // New FOT Status Filter const matchesFotStatus = !advancedFilters.fotStatus || (advancedFilters.fotStatus === 'finalizada' && e.fot_finalizada) || (advancedFilters.fotStatus === 'pre_venda' && (e.fot_pre_venda || e.pre_venda)) || (advancedFilters.fotStatus === 'normal' && !e.fot_finalizada && !e.fot_pre_venda && !e.pre_venda); return ( matchesSearch && matchesStatus && matchesDate && matchesFot && matchesType && matchesCompany && matchesInstitution && matchesFotStatus ); }); // Keep selectedEvent in sync with global events state useEffect(() => { if (selectedEvent) { const updated = events.find((e) => e.id === selectedEvent.id); if (updated && updated !== selectedEvent) { setSelectedEvent(updated); } } }, [events, selectedEvent]); const handleApprove = (e: React.MouseEvent, eventId: string) => { e.stopPropagation(); updateEventStatus(eventId, EventStatus.CONFIRMED); }; const handleReject = (e: React.MouseEvent, eventId: string, reason?: string) => { e.stopPropagation(); updateEventStatus(eventId, EventStatus.ARCHIVED, reason); }; const handleAssignmentResponse = async ( e: React.MouseEvent, eventId: string, status: string, reason?: string ) => { e.stopPropagation(); // Validação de Lotação da Equipe (Apenas para Aceite) if (status === "ACEITO") { const targetEvent = events.find(evt => evt.id === eventId); const currentProfessional = professionals.find(p => p.usuarioId === user.id); if (targetEvent && currentProfessional) { // Reutilizando lógica de contagem (adaptada de calculateTeamStatus) // Precisamos recalcular pois calculateTeamStatus depende de selectedEvent que pode não ser o alvo aqui const assignments = targetEvent.assignments || []; const hasRole = (professional: Professional | undefined, roleSlug: string) => { if (!professional) return false; const term = roleSlug.toLowerCase(); if (professional.functions && professional.functions.length > 0) { return professional.functions.some(f => f.nome.toLowerCase().includes(term)); } return (professional.role || "").toLowerCase().includes(term); }; // Helper to check if assignment handles a specific role (using ID first, then fallback) const isAssignedToRole = (assignment: any, roleSlug: string) => { if (assignment.funcaoId && functions) { const func = functions.find(f => f.id === assignment.funcaoId); if (func) return func.nome.toLowerCase().includes(roleSlug.toLowerCase()); } // Fallback only if no function assigned if (!assignment.funcaoId) { const p = professionals.find(pr => pr.id === assignment.professionalId); return hasRole(p, roleSlug); } return false; }; // Contagens Atuais (Updated to match EventTable logic) const acceptedFot = assignments.filter(a => a.status === "ACEITO" && isAssignedToRole(a, "fot")).length; const acceptedRecep = assignments.filter(a => a.status === "ACEITO" && isAssignedToRole(a, "recep")).length; const acceptedCine = assignments.filter(a => a.status === "ACEITO" && isAssignedToRole(a, "cine")).length; // Limites const reqFot = targetEvent.qtdFotografos || 0; const reqRecep = targetEvent.qtdRecepcionistas || 0; const reqCine = targetEvent.qtdCinegrafistas || 0; // Find OUR assignment to know what we are accepting const myAssignment = assignments.find(a => a.professionalId === currentProfessional.id); const checkQuota = (roleSlugs: string[], acceptedCount: number, requiredCount: number, roleName: string) => { // Only check quota if I am assigned to this role if (myAssignment) { if (isAssignedToRole(myAssignment, roleSlugs[0])) { // Using first slug as proxy for role group if (acceptedCount >= requiredCount) { return `A equipe de ${roleName} já está completa (${acceptedCount}/${requiredCount}).`; } } } else { // Fallback if no assignment found (shouldn't happen for existing invite) const isProfessionalRole = roleSlugs.some(slug => hasRole(currentProfessional, slug)); if (isProfessionalRole && acceptedCount >= requiredCount) { return `A equipe de ${roleName} já está completa (${acceptedCount}/${requiredCount}).`; } } return null; }; const errors = []; const errFot = checkQuota(["fot"], acceptedFot, reqFot, "Fotografia"); if (errFot) errors.push(errFot); const errRecep = checkQuota(["recep"], acceptedRecep, reqRecep, "Recepção"); if (errRecep) errors.push(errRecep); const errCine = checkQuota(["cine"], acceptedCine, reqCine, "Cinegrafia"); if (errCine) errors.push(errCine); if (errors.length > 0) { alert(`Não foi possível aceitar o convite:\n\n${errors.join("\n")}\n\nEntre em contato com a administração.`); return; // Bloqueia a ação } } } await respondToAssignment(eventId, status, reason); }; const handleOpenMaps = () => { if (!selectedEvent) return; if (selectedEvent.address.mapLink) { window.open(selectedEvent.address.mapLink, "_blank"); return; } const { street, number, city, state } = selectedEvent.address; const query = encodeURIComponent( `${street}, ${number}, ${city} - ${state}` ); window.open( `https://www.google.com/maps/search/?api=1&query=${query}`, "_blank" ); }; const handleApproveEvent = async (e: React.MouseEvent) => { e.stopPropagation(); if (selectedEvent) { if(window.confirm("Tem certeza que deseja aprovar este evento?")) { await updateEventStatus(selectedEvent.id, EventStatus.CONFIRMED); // Optimistically update setSelectedEvent({ ...selectedEvent, status: EventStatus.CONFIRMED }); } } }; const handleFinalizeClass = async (e: React.MouseEvent, eventToFinalize?: EventData) => { e.stopPropagation(); const targetEvent = eventToFinalize || selectedEvent; if (!targetEvent || !targetEvent.fotId) return; // Check permissions (Optional, backend also checks) if (user.role !== UserRole.SUPERADMIN && user.role !== UserRole.BUSINESS_OWNER) { alert("Você não tem permissão para finalizar turmas."); return; } const isFinalized = targetEvent.fot_finalizada; const action = isFinalized ? "reabrir" : "finalizar"; if(window.confirm(`Tem certeza que deseja ${action} a turma ${targetEvent.fot}?`)) { try { if (!token) { alert("Erro de autenticação. Tente fazer login novamente."); return; } await finalizeFOT(targetEvent.fotId, !isFinalized, token); alert(`Turma ${action === "reabrir" ? "reaberta" : "finalizada"} com sucesso!`); // Refresh events list if (refreshEvents) { refreshEvents(); } // Update selected event if it matches if (selectedEvent && selectedEvent.fotId === targetEvent.fotId) { setSelectedEvent({ ...selectedEvent, fot_finalizada: !isFinalized }); } } catch (error: any) { alert("Erro ao alterar status da turma: " + error.message); } } }; const handleManageTeam = () => { setIsTeamModalOpen(true); }; 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); if (!assignment && prof && prof.functions && prof.functions.length > 1) { setRoleSelectionProf(prof); return; } 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 = async (funcaoId: string) => { if (roleSelectionProf && selectedEvent) { 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); } } }; // --- RENDERS PER ROLE --- const renderRoleSpecificHeader = () => { if (user.role === UserRole.EVENT_OWNER) { return (

Meus Eventos

Acompanhe seus eventos ou solicite novos orçamentos.

); } if (user.role === UserRole.PHOTOGRAPHER) { return (

Eventos Designados

Gerencie seus trabalhos e visualize detalhes.

); } return (

Gestão Geral

Controle total de eventos, aprovações e equipes.

); }; const renderRoleSpecificActions = () => { if (user.role === UserRole.PHOTOGRAPHER || user.role === UserRole.AGENDA_VIEWER || user.role === UserRole.RESEARCHER) return null; const label = user.role === UserRole.EVENT_OWNER ? "Solicitar Novo Evento" : "Novo Evento"; return ( ); }; // --- MAIN RENDER --- return (
{/* Header */} {view === "list" && !new URLSearchParams(window.location.search).get("eventId") && (
{renderRoleSpecificHeader()} {renderRoleSpecificActions()}
)} {/* Content Switcher */} {view === "list" && !new URLSearchParams(window.location.search).get("eventId") && (
{/* Search Bar */}
setSearchTerm(e.target.value)} />
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && (
)}
{/* Advanced Filters */} {/* Results Count */}
Exibindo{" "} {filteredEvents.length} {" "} de {myEvents.length} eventos
{/* Event Table */} { setSelectedEvent(event); setView("details"); }} onApprove={handleApprove} onReject={handleReject} userRole={user.role} currentProfessionalId={currentProfessionalId} onAssignmentResponse={handleAssignmentResponse} isManagingTeam={true} // Permitir aprovação na gestão geral professionals={professionals} // Adicionar lista de profissionais functions={functions} isLoading={isLoading} onFinalizeClass={handleFinalizeClass} />
)} {(view === "create" || view === "edit") && ( setView(view === "edit" ? "details" : "list")} onSubmit={handleSaveEvent} initialData={view === "edit" ? selectedEvent : undefined} /> )} {/* Loading State for Deep Link */ } {(!!new URLSearchParams(window.location.search).get("eventId") && (!selectedEvent || view !== "details")) && (

Carregando detalhes do evento...

)} {view === "details" && selectedEvent && (
{/* Status Banner */} {selectedEvent.status === EventStatus.PENDING_APPROVAL && user.role === UserRole.EVENT_OWNER && (

Solicitação em Análise

Seu evento foi enviado e está aguardando aprovação da equipe Photum.

)}
{/* Header Section - Sem foto */}

{selectedEvent.name}

{new Date( selectedEvent.date + "T00:00:00" ).toLocaleDateString("pt-BR")}{" "} às {selectedEvent.time} {selectedEvent.address.city},{" "} {selectedEvent.address.state}
{/* Badge de Finalizada */} {(selectedEvent.fot_finalizada) && (
Turma Finalizada
)} {/* Badge de Pré-Venda */} {((selectedEvent.fot_pre_venda || selectedEvent.pre_venda) && !selectedEvent.fot_finalizada) && (
Pré-Venda
)}
{selectedEvent.status}
{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && ( <> )} {user.role === UserRole.EVENT_OWNER && selectedEvent.status !== EventStatus.ARCHIVED && ( )} {/* Botão de Aprovação (Apenas Admin/Owner se pendente) */} {(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && selectedEvent.status === EventStatus.PENDING_APPROVAL && ( )}
{/* Quick Info Cards */}

Tipo

{selectedEvent.type}

Data

{new Date( selectedEvent.date + "T00:00:00" ).toLocaleDateString("pt-BR")}

Horário

{selectedEvent.time}

{basePrice !== null && (
0 ? "bg-green-50 border-green-200" : "bg-gray-50 border-gray-200"}`}>

0 ? "text-green-700" : "text-gray-500"}`}> Valor Base

0 ? "text-green-800" : "text-gray-700"}`}> {basePrice > 0 ? basePrice.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }) : "R$ 0,00"}

)}
{/* FOT Information Table */}

Informações FOT

{/* Seção de Gestão de Equipe - Ocultar para Fotógrafos, mas mostrar para Coordenadores */} {(user.role !== UserRole.PHOTOGRAPHER || (selectedEvent.assignments || []).some((a: any) => a.professionalId === (user.professionalId || user.id) && a.is_coordinator)) && ( <> {/* Helper to calculate pending counts */ (() => { const teamStatus = calculateTeamStatus(selectedEvent); const renderResourceRow = (label: string, confirmed: number, pending: number, required: number) => { if (!required && confirmed === 0 && pending === 0) { return ( ); } const isComplete = confirmed >= required; return ( ); }; return ( <> {renderResourceRow( "Fotógrafo", teamStatus.acceptedFotografos, teamStatus.pendingFotografos, selectedEvent.qtdFotografos || 0 )} {renderResourceRow( "Recepcionista", teamStatus.acceptedRecepcionistas, teamStatus.pendingRecepcionistas, selectedEvent.qtdRecepcionistas || 0 )} {renderResourceRow( "Cinegrafista", teamStatus.acceptedCinegrafistas, teamStatus.pendingCinegrafistas, selectedEvent.qtdCinegrafistas || 0 )} ); })()} {/* Status e Faltantes */} {(() => { const teamStatus = calculateTeamStatus(selectedEvent); return ( <> ); })()} )} {/* Base Value Card for Professionals */} {basePrice !== null && basePrice > 0 && ( )}
FOT {(selectedEvent as any).fot || "-"}
Data {new Date( selectedEvent.date + "T00:00:00" ).toLocaleDateString("pt-BR")}
Curso {selectedEvent.curso || "-"}
Instituição {selectedEvent.instituicao || "-"}
Ano Formatura {selectedEvent.anoFormatura || "-"}
Empresa {selectedEvent.empresa || "-"}
Tipo Evento {selectedEvent.type}
Observações {(selectedEvent as any).observacoes || "-"}
Local {(selectedEvent as any).local_evento || (selectedEvent as any).locationName || "-"}
Endereço {selectedEvent.address.street}, {selectedEvent.address.number} - {selectedEvent.address.city}/{selectedEvent.address.state} {selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`}
QTD Formandos {selectedEvent.qtdFormandos || (selectedEvent as any).qtd_formandos || selectedEvent.attendees || "-"}
Gestão de Equipe e Recursos
{label} -
{label}
{confirmed} / {required || 0} {isComplete && }
{pending > 0 && ( ({pending} aguardando aceite) )}
Estúdio {selectedEvent.qtdEstudios || (selectedEvent as any).qtd_estudios || "-"}
Ponto de Foto {selectedEvent.qtdPontosFoto || (selectedEvent as any).qtd_ponto_foto || "-"}
Ponto Decorado {selectedEvent.qtdPontosDecorados || (selectedEvent as any).qtd_ponto_decorado || "-"}
Ponto LED {selectedEvent.qtdPontosLed || (selectedEvent as any).qtd_pontos_led || "-"}
Plataforma 360 {selectedEvent.qtdPlataforma360 || (selectedEvent as any).qtd_plataforma_360 || "-"}
Profissionais OK? {teamStatus.profissionaisOK ? ( <> Completo ) : ( <> Incompleto )}
Foto Faltante 0 ? "text-red-600" : "text-green-600"}`}> {teamStatus.fotoFaltante}
Recep. Faltante 0 ? "text-red-600" : "text-green-600"}`}> {teamStatus.recepFaltante}
Cine Faltante 0 ? "text-red-600" : "text-green-600"}`}> {teamStatus.cineFaltante}
Valor Base {basePrice.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
Horário {selectedEvent.time}
{/* Institution Information */} {selectedEvent.institutionId && (() => { const institution = getInstitutionById( selectedEvent.institutionId ); if (institution) { return (

{institution.name}

{institution.type}

{/* Course Information */} {selectedEvent.courseId && (() => { const course = getActiveCoursesByInstitutionId( selectedEvent.institutionId ).find( (c) => c.id === selectedEvent.courseId ); if (course) { return (

Curso/Turma

{course.name} -{" "} {course.graduationType} ( {course.year}/{course.semester}º sem)

); } return null; })()}

Contato

{institution.phone}

{institution.email}

{institution.address && (

Endereço

{institution.address.street},{" "} {institution.address.number}

{institution.address.city} -{" "} {institution.address.state}

)}
{institution.description && (

{institution.description}

)}
); } return null; })()}

Sobre o Evento

{selectedEvent.briefing || "Sem briefing detalhado."}

{selectedEvent.contacts.length > 0 && (

Contatos & Responsáveis

{selectedEvent.contacts.map((c, i) => (

{c.name}

{c.role}

{c.phone}

))}
)}
{/* Localização Card */}

Localização

{selectedEvent.address.street},{" "} {selectedEvent.address.number}

{selectedEvent.address.city} -{" "} {selectedEvent.address.state}

{selectedEvent.address.zip && (

CEP: {selectedEvent.address.zip}

)}
{/* Equipe Designada */} {(selectedEvent.assignments && selectedEvent.assignments.filter(a => a.status !== "REJEITADO").length > 0) || ((user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && selectedEvent.photographerIds.length > 0) ? (

Equipe ({(selectedEvent.assignments || []).filter(a => a.status !== "REJEITADO").length})

{(user.role === UserRole.BUSINESS_OWNER || user.role === UserRole.SUPERADMIN) && ( )}
{(selectedEvent.assignments || []) .filter(a => a.status !== "REJEITADO") .map((assignment) => { const photographer = professionals.find( (p) => p.id === assignment.professionalId ); return (
handleViewProfessional(photographer!)} >
{assignment.is_coordinator && (
)}
{photographer?.name || "Fotógrafo"} {assignment.is_coordinator && ( Coord. )} {assignment.status === "PENDENTE" ? "Convite Pendente" : "Confirmado"} {assignment.funcaoId && functions && ( {functions.find(f => f.id === assignment.funcaoId)?.nome || ""} )}
{assignment.status === "PENDENTE" && ( )} {assignment.status === "ACEITO" && ( )}
); })} {(selectedEvent.assignments || []).filter(a => a.status !== "REJEITADO").length === 0 && (

Nenhum profissional na equipe.

)}
) : null }
)} {/* Modal de Gerenciamento de Equipe */} {isTeamModalOpen && selectedEvent && (
{/* Header do Modal */}

Gerenciar Equipe

{selectedEvent.name} - {new Date(selectedEvent.date + 'T00:00:00').toLocaleDateString('pt-BR')}

{/* Resource Summary Bar */}
{(() => { const stats = calculateTeamStatus(selectedEvent); return ( <>
Fotógrafos:
0 ? 'text-red-600' : 'text-green-600'}`}> {stats.acceptedFotografos} / {stats.qtdFotografos} {stats.fotoFaltante === 0 ? ( ) : ( )}
Cinegrafistas:
0 ? 'text-red-600' : 'text-green-600'}`}> {stats.acceptedCinegrafistas} / {stats.qtdCinegrafistas} {stats.cineFaltante === 0 ? ( ) : ( )}
Recepcionistas:
0 ? 'text-red-600' : 'text-green-600'}`}> {stats.acceptedRecepcionistas} / {stats.qtdRecepcionistas} {stats.recepFaltante === 0 ? ( ) : ( )}
); })()}
{/* Body */}

Profissionais disponíveis para a data{" "} {new Date( selectedEvent.date + "T00:00:00" ).toLocaleDateString("pt-BR")} . Clique em "Adicionar" para atribuir ao evento.

{/* Filtros e Busca */}
{/* Barra de busca */}
setTeamSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple focus:border-transparent" />
{/* Filtros */}
{/* Filtro por função */} {/* Filtro por status */} {/* Filtro por disponibilidade */} {/* Botão limpar filtros */} {(teamSearchTerm || teamRoleFilter !== "all" || teamStatusFilter !== "all" || teamAvailabilityFilter !== "all") && ( )}
{/* Tabela de Profissionais (Desktop) */}
{displayedTeamProfessionals.map((photographer) => { const assignment = (selectedEvent.assignments || []).find( (a) => a.professionalId === photographer.id ); const status = assignment ? assignment.status : null; const isAssigned = !!status && status !== "REJEITADO"; // 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 isProcessing = processingIds.has(photographer.id); return ( {/* Profissional */} {/* Função */} {/* E-mail */} {/* Status */} {/* Ação */} ); })} {/* Empty State */} {filteredTeamProfessionals.length === 0 && ( )} {/* Load More Row */} {displayedTeamProfessionals.length < filteredTeamProfessionals.length && ( )}
Profissional Função E-mail Status Ação
handleViewProfessional(photographer)}>
{/* Avatar */}
{assignment?.is_coordinator && (
)}

{photographer.name || photographer.nome}

ID: {photographer.id.substring(0, 8)}...

{photographer.functions && photographer.functions.length > 0 ? photographer.functions.map(f => f.nome).join(", ") : photographer.role} {photographer.email} {status === "ACEITO" && ( Confirmado )} {status === "PENDENTE" && ( Pendente )} {status === "REJEITADO" && ( Recusado )} {!status && ( {isBusy ? : } {isBusy ? "Em outro evento" : "Disponível"} )}
{(status === "ACEITO" || status === "PENDENTE") && ( )}
{/* ... existing empty state ... */}

Ninguém encontrado.

{/* Lista de Cards (Mobile) */}
{displayedTeamProfessionals.map((photographer) => { const assignment = (selectedEvent.assignments || []).find( (a) => a.professionalId === photographer.id ); const status = assignment ? assignment.status : null; const isAssigned = !!status && status !== "REJEITADO"; const isBusyInSet = busyProfessionalIds.has(photographer.id); const isBusy = !isAssigned && isBusyInSet; const isProcessing = processingIds.has(photographer.id); return (
handleViewProfessional(photographer)}>
{/* Visual Indicator of Coordinator in the list */} {assignment?.is_coordinator && (
)}

{photographer.name || photographer.nome}

{assignment?.status === "PENDENTE" ? "Convite Pendente" : assignment?.status}

{photographer.functions && photographer.functions.length > 0 ? photographer.functions.map(f => f.nome).join(", ") : photographer.role} {status === "ACEITO" && ( Confirmado )} {status === "PENDENTE" && ( Pendente )} {status === "REJEITADO" && ( Recusado )} {!status && ( {isBusy ? : } {isBusy ? "Em outro evento" : "Disponível"} )}
); })} {/* Load More Mobile */} {displayedTeamProfessionals.length < filteredTeamProfessionals.length && (
)} {filteredTeamProfessionals.length === 0 && (

Nenhum profissional disponível.

)}
{/* Footer */}
)} {viewingProfessional && ( setViewingProfessional(null)} /> )} {roleSelectionProf && (

Selecione a Função

Qual função {roleSelectionProf.nome} irá desempenhar neste evento?

{roleSelectionProf.functions?.map((fn) => ( ))}
)}
); };