- Backend: Migrations para tabelas 'escalas' e 'logistica' (transporte) - Backend: Handlers e Services completos para gestão de escalas e logística - Backend: Suporte a auth vinculado a perfil profissional - Frontend: Nova página de Detalhes Operacionais (/agenda/:id) - Frontend: Componente EventScheduler com verificação robusta de conflitos - Frontend: Componente EventLogistics para gestão de motoristas e caronas - Frontend: Modal de Detalhes de Profissional unificado (Admin + Self-view) - Frontend: Dashboard com modal de gestão de equipe e filtros avançados - Fix: Correção crítica de timezone (UTC) em horários de agendamento - Fix: Tratamento de URLs no campo de local do evento - Fix: Filtros de profissional com carro na logística
159 lines
8.5 KiB
TypeScript
159 lines
8.5 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { ArrowLeft, MapPin, Calendar, Clock, DollarSign } from 'lucide-react';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
import { getAgendas } from '../services/apiService';
|
|
import EventScheduler from '../components/EventScheduler';
|
|
import EventLogistics from '../components/EventLogistics';
|
|
|
|
const EventDetails: React.FC = () => {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const { token } = useAuth();
|
|
const [event, setEvent] = useState<any | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [calculatedStats, setCalculatedStats] = useState({ studios: 0 });
|
|
|
|
useEffect(() => {
|
|
if (id && token) {
|
|
loadEvent();
|
|
}
|
|
}, [id, token]);
|
|
|
|
const loadEvent = async () => {
|
|
// Since we don't have a getEventById, we list and filter for now (MVP).
|
|
// Ideally backend should have GET /agenda/:id
|
|
const res = await getAgendas(token!);
|
|
if (res.data) {
|
|
const found = res.data.find((e: any) => e.id === id);
|
|
setEvent(found);
|
|
}
|
|
setLoading(false);
|
|
};
|
|
|
|
if (loading) return <div className="p-8 text-center">Carregando detalhes do evento...</div>;
|
|
if (!event) return <div className="p-8 text-center text-red-500">Evento não encontrado.</div>;
|
|
|
|
const formattedDate = new Date(event.data_evento).toLocaleDateString();
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 p-6">
|
|
<div className="max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center gap-4">
|
|
<button onClick={() => navigate('/eventos')} className="p-2 hover:bg-gray-200 rounded-full transition-colors">
|
|
<ArrowLeft className="w-6 h-6 text-gray-600" />
|
|
</button>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-800 flex items-center gap-2">
|
|
{event.empresa_nome} - {event.tipo_evento_nome}
|
|
<span className={`text-xs px-2 py-1 rounded-full ${event.status === 'Confirmado' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
|
{event.status}
|
|
</span>
|
|
</h1>
|
|
<p className="text-sm text-gray-500">ID: {event.fot_id}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Event Info Card (Spreadsheet Header Style) */}
|
|
<div className="bg-white rounded-lg shadow p-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 border-l-4 border-brand-purple">
|
|
<div className="flex items-start gap-3">
|
|
<Calendar className="w-5 h-5 text-brand-purple mt-1" />
|
|
<div>
|
|
<p className="text-xs text-gray-500 uppercase font-bold">Data</p>
|
|
<p className="font-medium text-gray-800">{formattedDate}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<MapPin className="w-5 h-5 text-brand-purple mt-1" />
|
|
<div>
|
|
<p className="text-xs text-gray-500 uppercase font-bold">Local:</p>
|
|
{(() => {
|
|
const localVal = event['local_evento'] || event.local || event.local_evento;
|
|
const isUrl = localVal && String(localVal).startsWith('http');
|
|
|
|
if (isUrl) {
|
|
return (
|
|
<a
|
|
href={localVal}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-medium text-brand-purple hover:underline truncate block"
|
|
title="Abrir no Mapa"
|
|
>
|
|
{event.locationName || "Ver Localização no Mapa"}
|
|
</a>
|
|
);
|
|
}
|
|
return <p className="font-medium text-gray-800">{localVal || "Não informado"}</p>;
|
|
})()}
|
|
<p className="text-xs text-gray-500">{event.endereco}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<Clock className="w-5 h-5 text-brand-purple mt-1" />
|
|
<div>
|
|
<p className="text-xs text-gray-500 uppercase font-bold">Horário</p>
|
|
<p className="font-medium text-gray-800">{event.horario}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-5 h-5 text-brand-purple mt-1 font-bold text-xs border border-brand-purple rounded flex items-center justify-center">?</div>
|
|
<div>
|
|
<p className="text-xs text-gray-500 uppercase font-bold">Observações</p>
|
|
<p className="text-xs text-gray-600 line-clamp-2">{event.observacoes_evento || "Nenhuma observação."}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Content: Scheduling & Logistics */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Left: Scheduling (Escala) */}
|
|
<EventScheduler
|
|
agendaId={id!}
|
|
dataEvento={event.data_evento.split('T')[0]}
|
|
allowedProfessionals={(event as any).assigned_professionals?.map((p: any) => p.professional_id)}
|
|
onUpdateStats={setCalculatedStats}
|
|
defaultTime={event.horario}
|
|
/>
|
|
|
|
{/* Right: Logistics (Carros) */}
|
|
<div className="space-y-6">
|
|
<EventLogistics
|
|
agendaId={id!}
|
|
assignedProfessionals={(event as any).assigned_professionals?.map((p: any) => p.professional_id)}
|
|
/>
|
|
|
|
{/* Equipment / Studios Section (Placeholder for now based on spreadsheet) */}
|
|
<div className="bg-white p-4 rounded-lg shadow">
|
|
<h3 className="text-lg font-semibold text-gray-800 mb-2 flex items-center">
|
|
<DollarSign className="w-5 h-5 mr-2 text-green-600" /> {/* Using DollarSign as generic icon for assets/inventory for now, map to Camera later */}
|
|
Equipamentos & Estúdios
|
|
</h3>
|
|
<div className="bg-gray-50 p-3 rounded text-sm space-y-2">
|
|
<div className="flex justify-between border-b pb-2">
|
|
<span className="text-gray-600">Qtd. Estúdios (Automático):</span>
|
|
<span className="font-bold text-indigo-600">{calculatedStats.studios}</span>
|
|
</div>
|
|
<div className="flex justify-between border-b pb-2">
|
|
<span className="text-gray-600">Ponto de Foto:</span>
|
|
<span className="font-bold">{event.qtd_ponto_foto || 0}</span>
|
|
</div>
|
|
<div className="mt-2">
|
|
<p className="text-xs text-gray-500 font-bold mb-1">Notas de Equipamento:</p>
|
|
<textarea
|
|
className="w-full text-xs p-2 rounded border bg-white"
|
|
placeholder="Ex: Levar 2 tripés extras..."
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default EventDetails;
|