import React, { useState, useEffect, useMemo } from 'react'; import { useData } from '../contexts/DataContext'; import { EventData, EventStatus, Professional } from '../types'; import { RefreshCw, Users, CheckCircle, AlertTriangle, Calendar as CalendarIcon, MapPin, Clock, UserPlus, ChevronLeft, ChevronRight, ChevronDown, ChevronUp } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { getAvailableProfessionalsLogistics, createDailyInvitation, getDailyInvitationsByDate } from '../services/apiService'; import { toast } from 'react-hot-toast'; import { ProfessionalDetailsModal } from '../components/ProfessionalDetailsModal'; import { Car } from 'lucide-react'; // Added Car icon export const DailyLogistics: React.FC = () => { const { events, professionals, isLoading, refreshEvents } = useData(); const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); const navigate = useNavigate(); const [searchTerm, setSearchTerm] = useState(""); const [dailyInvitations, setDailyInvitations] = useState([]); const [isInvitationsExpanded, setIsInvitationsExpanded] = useState(true); const [viewingProfessional, setViewingProfessional] = useState(null); const [isProfModalOpen, setIsProfModalOpen] = useState(false); // Utility para cores de cidade baseado na planilha do cliente + cores dinâmicas const getCityColor = (city: string | undefined) => { if (!city) return 'bg-gray-100 text-gray-700 border-gray-200'; // Default const c = city.toLowerCase().trim(); if (c.includes('campinas')) return 'bg-pink-100 text-pink-700 border-pink-200'; if (c.includes('piracicaba')) return 'bg-cyan-100 text-cyan-700 border-cyan-200'; if (c.includes('paulo') || c.includes('sp')) return 'bg-slate-200 text-slate-700 border-slate-300'; if (c.includes('americana') || c.includes('sbo') || c.includes('barbara') || c.includes('bárbara') || c.includes('santa barbara')) return 'bg-green-100 text-green-700 border-green-200'; if (c.includes('indaiatuba')) return 'bg-yellow-100 text-yellow-700 border-yellow-200'; // Dynamic color for others const palette = [ 'bg-purple-100 text-purple-700 border-purple-200', 'bg-indigo-100 text-indigo-700 border-indigo-200', 'bg-teal-100 text-teal-700 border-teal-200', 'bg-orange-100 text-orange-700 border-orange-200', 'bg-rose-100 text-rose-700 border-rose-200', 'bg-violet-100 text-violet-700 border-violet-200', 'bg-emerald-100 text-emerald-700 border-emerald-200', 'bg-fuchsia-100 text-fuchsia-700 border-fuchsia-200' ]; let hash = 0; for (let i = 0; i < c.length; i++) { hash = c.charCodeAt(i) + ((hash << 5) - hash); } return palette[Math.abs(hash) % palette.length]; }; const handleDateChange = (e: React.ChangeEvent) => { setSelectedDate(e.target.value); }; const handleRefresh = () => { refreshEvents(); }; // Filter events by the selected date const eventsInDay = useMemo(() => { return events.filter(e => { if (!e.date) return false; // e.date might be YYYY-MM-DD or full ISO const eventDateStr = e.date.split('T')[0]; return eventDateStr === selectedDate; }); }, [events, selectedDate]); // Aggregate metrics const metrics = useMemo(() => { let totalNeeded = 0; let totalAccepted = 0; let totalPending = 0; // Detailed Role missing counts let fotMissing = 0; let recMissing = 0; let cinMissing = 0; // Track professionals already counted in events for this day const eventAcceptedProfIds = new Set(); const eventPendingProfIds = new Set(); eventsInDay.forEach(event => { // Required professionals const fotReq = Number(event.qtdFotografos || 0); const recReq = Number(event.qtdRecepcionistas || 0); const cinReq = Number(event.qtdCinegrafistas || 0); const eventTotalNeeded = fotReq + recReq + cinReq; totalNeeded += eventTotalNeeded; let fotAc = 0, recAc = 0, cinAc = 0; let eTotalAc = 0, eTotalPd = 0; // Count assignments if (event.assignments) { event.assignments.forEach(assignment => { const status = assignment.status ? String(assignment.status).toUpperCase() : "AGUARDANDO"; const prof = professionals.find(p => p.id === assignment.professionalId); const roleName = prof?.role?.toLowerCase() || ""; if (status === "ACEITO" || status === "CONFIRMADO") { eTotalAc++; eventAcceptedProfIds.add(assignment.professionalId); if (roleName.includes('fot')) fotAc++; else if (roleName.includes('rece')) recAc++; else if (roleName.includes('cin')) cinAc++; else if (assignment.funcaoId === 'fotografo_id') fotAc++; } else if (status === "AGUARDANDO" || status === "PENDENTE") { eTotalPd++; eventPendingProfIds.add(assignment.professionalId); } }); } totalAccepted += eTotalAc; totalPending += eTotalPd; // Calculate missing per role for this event fotMissing += Math.max(0, fotReq - fotAc); recMissing += Math.max(0, recReq - recAc); cinMissing += Math.max(0, cinReq - cinAc); }); // Add Daily Invitations to the generic counters only if not already allocated in an event const uniqueDailyPending = dailyInvitations.filter(i => i.status === 'PENDENTE' && !eventPendingProfIds.has(i.profissional_id) && !eventAcceptedProfIds.has(i.profissional_id)).length; const uniqueDailyAccepted = dailyInvitations.filter(i => i.status === 'ACEITO' && !eventAcceptedProfIds.has(i.profissional_id)).length; totalPending += uniqueDailyPending; totalAccepted += uniqueDailyAccepted; const totalMissing = Math.max(0, totalNeeded - totalAccepted); return { totalNeeded, totalAccepted, totalPending, totalMissing, fotMissing, recMissing, cinMissing }; }, [eventsInDay, professionals, dailyInvitations]); const [paginatedProfessionals, setPaginatedProfessionals] = useState([]); const [totalPros, setTotalPros] = useState(0); const [page, setPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [isProsLoading, setIsProsLoading] = useState(false); // Helper: Fetch available professionals from API const fetchPros = async () => { setIsProsLoading(true); try { const resp = await getAvailableProfessionalsLogistics(selectedDate, page, 50, searchTerm); setPaginatedProfessionals(resp.data || []); setTotalPros(resp.total || 0); setTotalPages(resp.total_pages || 1); } catch (err) { console.error("Failed to fetch professionals", err); toast.error("Erro ao carregar profissionais"); } finally { setIsProsLoading(false); } }; const fetchDailyInvitations = async () => { try { const res = await getDailyInvitationsByDate(selectedDate); setDailyInvitations(res || []); } catch(err) { console.error(err); } }; useEffect(() => { fetchPros(); fetchDailyInvitations(); }, [selectedDate, page, searchTerm]); // Select Event Context Handlers -> Now it's Daily Allocation! const allocDaily = async (profId: string) => { try { await createDailyInvitation(profId, selectedDate); toast.success("Convite de diária enviado!"); fetchPros(); // Refresh list to remove from available fetchDailyInvitations(); // refresh the pending invitations section } catch (err: any) { toast.error(err.message || "Erro ao alocar diária"); } }; // Render individual event card const renderEventCard = (event: EventData) => { const fotReq = Number(event.qtdFotografos || 0); const recReq = Number(event.qtdRecepcionistas || 0); const cinReq = Number(event.qtdCinegrafistas || 0); const needed = fotReq + recReq + cinReq; // Group accepted/pending counts by role directly from event's assignments // We assume assignments map back to the professional's functions or are inherently mapped let accepted = 0; let pending = 0; let fotAc = 0, recAc = 0, cinAc = 0; event.assignments?.forEach(a => { const status = a.status ? String(a.status).toUpperCase() : "AGUARDANDO"; const prof = professionals.find(p => p.id === a.professionalId); const roleName = prof?.role?.toLowerCase() || ""; if (status === "ACEITO" || status === "CONFIRMADO") { accepted++; if (roleName.includes('fot')) fotAc++; else if (roleName.includes('rece')) recAc++; else if (roleName.includes('cin')) cinAc++; else if (a.funcaoId === 'fotografo_id') fotAc++; // Fallback maps } else if (status === "AGUARDANDO" || status === "PENDENTE") pending++; }); const missing = Math.max(0, needed - accepted); return (
navigate(`/painel?eventId=${event.id}`)} className="bg-white rounded-xl shadow-sm border border-gray-100 p-5 hover:shadow-md hover:border-brand-purple/50 transition-all cursor-pointer group" >

{event.name}

{event.startTime || event.time}{event.endTime || event.horario_fim ? ` às ${event.endTime || event.horario_fim}` : ''} {event.address?.city || "Cidade N/A"}
0 ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}> {missing > 0 ? `Faltam ${missing}` : 'Completo'}
{/* Roles Breakdown */}
Função
A / N
{fotReq > 0 && (
Fotógrafo
{fotAc} / {fotReq}
)} {recReq > 0 && (
Recepcionista
{recAc} / {recReq}
)} {cinReq > 0 && (
Cinegrafista
{cinAc} / {cinReq}
)} {needed === 0 && (
Nenhuma vaga registrada
)}
{Array.isArray(event.assignments) && event.assignments.length > 0 && (
Total de Convites: {event.assignments.length}
{pending > 0 && (
Aguardando resposta: {pending}
)}
)}
); }; return (

Logística Diária

Acompanhamento e escala de equipe por data da agenda

{/* Dash/Metrics Panel */}

Necessários

{metrics.totalNeeded}

Aceitos

{metrics.totalAccepted}

Pendentes

{metrics.totalPending}

Faltam (Geral)

{metrics.totalMissing}

{/* Detailed Missing Roles */} {metrics.totalMissing > 0 && (
{metrics.fotMissing > 0 && ( Fotógrafos: {metrics.fotMissing} )} {metrics.recMissing > 0 && ( Recepcionistas: {metrics.recMissing} )} {metrics.cinMissing > 0 && ( Cinegrafistas: {metrics.cinMissing} )}
)}
{/* Daily Invitations Section */} {dailyInvitations.length > 0 && (
setIsInvitationsExpanded(!isInvitationsExpanded)} >

Convites Enviados ({dailyInvitations.length})

{isInvitationsExpanded ? : }
{isInvitationsExpanded && (
{dailyInvitations.map((inv: any) => { const prof = professionals.find(p => p.id === inv.profissional_id); return (
{ if (prof) { setViewingProfessional(prof); setIsProfModalOpen(true); } }} >
{prof?.nome}

{prof?.nome || 'Desconhecido'}

{prof?.carro_disponivel && ( )}

{prof?.role || 'Profissional'}

{prof?.cidade && ( {prof.cidade} )}
{inv.status}
) })}
)}
)} {/* Layout with Events (Left) and Available Professionals Sidebar (Right) */}
{/* Left Column: Events */}
{isLoading ? (
) : eventsInDay.length === 0 ? (

Nenhum evento nesta data

Selecione outro dia no calendário acima.

) : (
{eventsInDay.map(renderEventCard)}
)}
{/* Right Column: Sticky Sidebar for Professionals */}

Lista Geral de Disponíveis

Profissionais com status livre no dia {selectedDate.split('-').reverse().join('/')}

setSearchTerm(e.target.value)} className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-brand-purple outline-none" />
{isProsLoading ? (
) : paginatedProfessionals.length === 0 ? (

Nenhum profissional disponível.

Todos ocupados ou não encontrados.

) : ( paginatedProfessionals.map(prof => (
{ setViewingProfessional(prof); setIsProfModalOpen(true); }} > {prof.nome}

{prof.nome}

{prof.carro_disponivel && ( )}

{prof.role || "Profissional"}

{prof.cidade && ( {prof.cidade} )}
)) )}
{/* Pagination Context */} {totalPages > 1 && (
Página {page} de {totalPages}
)}
{/* Modal Profissionais */} {isProfModalOpen && viewingProfessional && ( { setIsProfModalOpen(false); setViewingProfessional(null); }} /> )}
); };