photum/frontend/pages/Dashboard.tsx
João Vitor 3178207353 feat: Sistema completo de Gestão FOT e melhorias no dashboard
- Sistema FOT (Formatura Operations Tracking):
  * Tela de Gestão FOT (/cursos) com tabela Excel-style
  * Modal CourseForm com 10 campos (FOT, Empresa, Instituição, etc)
  * Validação de FOT (5 dígitos numéricos)
  * Edição de turmas ao clicar na linha
  * Integração com API backend (empresas, níveis educacionais, universidades)

- Dashboard renovado (/painel):
  * Tabela com 8 colunas (FOT, Data, Curso, Instituição, Ano, Empresa, Tipo, Status)
  * Filtros avançados: FOT (busca numérica), Data, Tipo de Evento
  * Removidos filtros de Estado e Cidade
  * Página de detalhes com tabela vertical (12 informações)
  * Botão Aprovar redireciona para modal de equipe

- Sistema de Aprovação Dupla (/aprovacao):
  * 2 tabelas separadas por abas (Usuários Normais e Profissionais)
  * Coluna Universidade renomeada para Empresa
  * Coluna Função nos profissionais
  * Workflow de aprovação com atribuição de equipe

- Cadastro Profissional (/cadastro-profissional):
  * Formulário específico para fotógrafos
  * Dropdown de Função Profissional da API
  * Tratamento de erro quando backend offline

- Modal de Criar Evento:
  * Tipo de Evento como primeiro campo
  * Nome do Evento (Opcional) como segundo campo

- Componentes novos:
  * EventTable.tsx - Tabela de eventos com ordenação
  * EventFiltersBar.tsx - Filtros avançados (3 filtros)
  * CourseForm.tsx - Formulário FOT completo
  * ProfessionalForm.tsx - Cadastro profissional

- API Service:
  * Integração com backend Go
  * Endpoints: /api/empresas, /api/funcoes, /api/niveis-educacionais, /api/universidades, /graduation-years

- Documentação:
  * README.md principal atualizado
  * frontend/README.md atualizado
  * Documentação completa de componentes e features
2025-12-11 16:02:39 -03:00

1091 lines
44 KiB
TypeScript

import React, { useState, useEffect, useMemo } from "react";
import { UserRole, EventData, EventStatus, EventType } 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,
} from "lucide-react";
import { useAuth } from "../contexts/AuthContext";
import { useData } from "../contexts/DataContext";
import { STATUS_COLORS } from "../constants";
interface DashboardProps {
initialView?: "list" | "create";
}
interface Professional {
id: string;
name: string;
email: string;
avatar: string;
role: "Fotógrafo" | "Cinegrafista" | "Recepcionista";
availability: {
[date: string]: boolean;
};
}
// Mock de profissionais cadastrados
const MOCK_PHOTOGRAPHERS: Professional[] = [
{
id: "photographer-1",
name: "Carlos Silva",
email: "carlos@photum.com",
avatar: "https://i.pravatar.cc/150?u=carlos",
role: "Fotógrafo",
availability: {
"2025-12-05": true,
"2025-12-10": true,
"2025-12-15": false,
"2025-12-20": true,
},
},
{
id: "photographer-2",
name: "Ana Santos",
email: "ana@photum.com",
avatar: "https://i.pravatar.cc/150?u=ana",
role: "Fotógrafo",
availability: {
"2025-12-05": true,
"2025-12-10": false,
"2025-12-15": true,
"2025-12-20": true,
},
},
{
id: "photographer-3",
name: "João Oliveira",
email: "joao@photum.com",
avatar: "https://i.pravatar.cc/150?u=joao",
role: "Cinegrafista",
availability: {
"2025-12-05": false,
"2025-12-10": true,
"2025-12-15": true,
"2025-12-20": false,
},
},
{
id: "photographer-4",
name: "Maria Costa",
email: "maria@photum.com",
avatar: "https://i.pravatar.cc/150?u=maria",
role: "Fotógrafo",
availability: {
"2025-12-05": true,
"2025-12-10": true,
"2025-12-15": true,
"2025-12-20": true,
},
},
{
id: "photographer-5",
name: "Pedro Alves",
email: "pedro@photum.com",
avatar: "https://i.pravatar.cc/150?u=pedro",
role: "Cinegrafista",
availability: {
"2025-12-05": false,
"2025-12-10": false,
"2025-12-15": true,
"2025-12-20": true,
},
},
{
id: "receptionist-1",
name: "Julia Mendes",
email: "julia@photum.com",
avatar: "https://i.pravatar.cc/150?u=julia",
role: "Recepcionista",
availability: {
"2025-12-05": true,
"2025-12-10": true,
"2025-12-15": true,
"2025-12-20": false,
},
},
{
id: "receptionist-2",
name: "Rafael Souza",
email: "rafael@photum.com",
avatar: "https://i.pravatar.cc/150?u=rafael",
role: "Recepcionista",
availability: {
"2025-12-05": true,
"2025-12-10": true,
"2025-12-15": false,
"2025-12-20": true,
},
},
{
id: "videographer-1",
name: "Lucas Ferreira",
email: "lucas@photum.com",
avatar: "https://i.pravatar.cc/150?u=lucas",
role: "Cinegrafista",
availability: {
"2025-12-05": true,
"2025-12-10": false,
"2025-12-15": true,
"2025-12-20": true,
},
},
];
export const Dashboard: React.FC<DashboardProps> = ({
initialView = "list",
}) => {
const { user } = useAuth();
const {
events,
getEventsByRole,
addEvent,
updateEventStatus,
assignPhotographer,
getInstitutionById,
getActiveCoursesByInstitutionId,
} = useData();
const [view, setView] = useState<"list" | "create" | "edit" | "details">(
initialView
);
const [searchTerm, setSearchTerm] = useState("");
const [selectedEvent, setSelectedEvent] = useState<EventData | null>(null);
const [activeFilter, setActiveFilter] = useState<string>("all");
const [advancedFilters, setAdvancedFilters] = useState<EventFilters>({
date: "",
fotId: "",
type: "",
});
const [isTeamModalOpen, setIsTeamModalOpen] = useState(false);
// Reset view when initialView prop changes
useEffect(() => {
if (initialView) {
setView(initialView);
if (initialView === "create") setSelectedEvent(null);
}
}, [initialView]);
// Guard Clause for basic security
if (!user)
return <div className="p-10 text-center">Acesso Negado. Faça login.</div>;
const myEvents = getEventsByRole(user.id, user.role);
// Extract unique values for filters
const { availableTypes } = useMemo(() => {
const types = [...new Set(myEvents.map((e) => e.type))].sort();
return {
availableTypes: types,
};
}, [myEvents]);
// Filter 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 as any).fotId || '').includes(advancedFilters.fotId);
const matchesType =
!advancedFilters.type || e.type === advancedFilters.type;
return (
matchesSearch &&
matchesStatus &&
matchesDate &&
matchesFot &&
matchesType
);
});
const handleSaveEvent = (data: any) => {
const isClient = user.role === UserRole.EVENT_OWNER;
if (view === "edit" && selectedEvent) {
const updatedEvent = { ...selectedEvent, ...data };
console.log("Updated", updatedEvent);
setSelectedEvent(updatedEvent);
setView("details");
} 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: [],
};
addEvent(newEvent);
setView("list");
}
};
const handleApprove = (e: React.MouseEvent, eventId: string) => {
e.stopPropagation();
const event = events.find((e) => e.id === eventId);
if (event) {
setSelectedEvent(event);
setView("details");
setIsTeamModalOpen(true);
}
};
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 handleManageTeam = () => {
setIsTeamModalOpen(true);
};
const togglePhotographer = (photographerId: string) => {
if (!selectedEvent) return;
assignPhotographer(selectedEvent.id, photographerId);
const updated = events.find((e) => e.id === selectedEvent.id);
if (updated) setSelectedEvent(updated);
};
// --- RENDERS PER ROLE ---
const renderRoleSpecificHeader = () => {
if (user.role === UserRole.EVENT_OWNER) {
return (
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black">
Meus Eventos
</h1>
<p className="text-gray-500 mt-1">
Acompanhe seus eventos ou solicite novos orçamentos.
</p>
</div>
);
}
if (user.role === UserRole.PHOTOGRAPHER) {
return (
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black">
Eventos Designados
</h1>
<p className="text-gray-500 mt-1">
Gerencie seus trabalhos e visualize detalhes.
</p>
</div>
);
}
return (
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black">
Gestão Geral
</h1>
<p className="text-gray-500 mt-1">
Controle total de eventos, aprovações e equipes.
</p>
</div>
);
};
const renderRoleSpecificActions = () => {
if (user.role === UserRole.PHOTOGRAPHER) return null;
const label =
user.role === UserRole.EVENT_OWNER
? "Solicitar Novo Evento"
: "Novo Evento";
return (
<Button onClick={() => setView("create")} className="shadow-lg">
<PlusCircle className="mr-2 h-5 w-5" /> {label}
</Button>
);
};
// --- MAIN RENDER ---
return (
<div className="min-h-screen bg-white pt-24 pb-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
{/* Header */}
{view === "list" && (
<div className="flex flex-col md:flex-row md:items-center justify-between mb-8 gap-4 fade-in">
{renderRoleSpecificHeader()}
{renderRoleSpecificActions()}
</div>
)}
{/* Content Switcher */}
{view === "list" && (
<div className="space-y-6 fade-in">
{/* Search Bar */}
<div className="flex flex-col sm:flex-row gap-4 items-center justify-between bg-gray-50 p-3 rounded-lg border border-gray-100">
<div className="relative flex-1 w-full">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<input
type="text"
placeholder="Buscar evento..."
className="w-full pl-10 pr-4 py-2 bg-white border border-gray-200 rounded-sm focus:outline-none focus:border-brand-gold text-sm"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{(user.role === UserRole.BUSINESS_OWNER ||
user.role === UserRole.SUPERADMIN) && (
<div className="flex space-x-2 bg-white p-1 rounded border border-gray-200">
<button
onClick={() => setActiveFilter("all")}
className={`px-3 py-1 text-xs font-medium rounded-sm ${
activeFilter === "all"
? "bg-brand-black text-white"
: "text-gray-600 hover:bg-gray-100"
}`}
>
Todos
</button>
<button
onClick={() => setActiveFilter("pending")}
className={`px-3 py-1 text-xs font-medium rounded-sm flex items-center ${
activeFilter === "pending"
? "bg-brand-gold text-white"
: "text-gray-600 hover:bg-gray-100"
}`}
>
<Clock size={12} className="mr-1" /> Pendentes
</button>
</div>
)}
</div>
{/* Advanced Filters */}
<EventFiltersBar
filters={advancedFilters}
onFilterChange={setAdvancedFilters}
availableTypes={availableTypes}
/>
{/* Results Count */}
<div className="flex items-center justify-between text-sm text-gray-600">
<span>
Exibindo{" "}
<strong className="text-brand-gold">
{filteredEvents.length}
</strong>{" "}
de <strong>{myEvents.length}</strong> eventos
</span>
</div>
{/* Event Table */}
<EventTable
events={filteredEvents}
onEventClick={(event) => {
setSelectedEvent(event);
setView("details");
}}
onApprove={handleApprove}
userRole={user.role}
/>
</div>
)}
{(view === "create" || view === "edit") && (
<EventForm
onCancel={() => setView(view === "edit" ? "details" : "list")}
onSubmit={handleSaveEvent}
initialData={view === "edit" ? selectedEvent : undefined}
/>
)}
{view === "details" && selectedEvent && (
<div className="fade-in">
<Button
variant="ghost"
onClick={() => setView("list")}
className="mb-4 pl-0"
>
Voltar para lista
</Button>
{/* Status Banner */}
{selectedEvent.status === EventStatus.PENDING_APPROVAL &&
user.role === UserRole.EVENT_OWNER && (
<div className="bg-yellow-50 border border-yellow-200 text-yellow-800 p-4 rounded-lg mb-6 flex items-start">
<Clock className="mr-3 flex-shrink-0" />
<div>
<h4 className="font-bold">Solicitação em Análise</h4>
<p className="text-sm mt-1">
Seu evento foi enviado e está aguardando aprovação da
equipe Photum.
</p>
</div>
</div>
)}
<div className="bg-white border rounded-lg overflow-hidden shadow-sm">
{/* Header Section - Sem foto */}
<div className="bg-gradient-to-r from-brand-gold/5 to-brand-black/5 border-b-2 border-brand-gold p-6">
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-serif font-bold text-brand-black mb-2">
{selectedEvent.name}
</h1>
<div className="flex flex-wrap gap-3 text-sm text-gray-600">
<span className="flex items-center gap-1">
<Calendar size={16} className="text-brand-gold" />
{new Date(
selectedEvent.date + "T00:00:00"
).toLocaleDateString("pt-BR")}{" "}
às {selectedEvent.time}
</span>
<span className="flex items-center gap-1">
<MapPin size={16} className="text-brand-gold" />
{selectedEvent.address.city},{" "}
{selectedEvent.address.state}
</span>
</div>
</div>
<div
className={`px-4 py-2 rounded text-sm font-semibold ${
STATUS_COLORS[selectedEvent.status]
}`}
>
{selectedEvent.status}
</div>
</div>
</div>
<div className="p-6">
{/* Actions Toolbar */}
<div className="flex flex-wrap gap-2 mb-6 pb-4 border-b">
{(user.role === UserRole.BUSINESS_OWNER ||
user.role === UserRole.SUPERADMIN) && (
<>
<Button
variant="outline"
onClick={() => setView("edit")}
className="text-sm"
>
<Edit size={16} className="mr-2" /> Editar Detalhes
</Button>
<Button
variant="outline"
onClick={handleManageTeam}
className="text-sm"
>
<Users size={16} className="mr-2" /> Gerenciar Equipe
</Button>
</>
)}
{user.role === UserRole.EVENT_OWNER &&
selectedEvent.status !== EventStatus.ARCHIVED && (
<Button
variant="outline"
onClick={() => setView("edit")}
className="text-sm"
>
<Edit size={16} className="mr-2" /> Editar Informações
</Button>
)}
<Button
variant="outline"
onClick={handleOpenMaps}
className="text-sm"
>
<Map size={16} className="mr-2" /> Abrir no Maps
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* Quick Info Cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">
Tipo
</p>
<p className="font-semibold text-gray-900">
{selectedEvent.type}
</p>
</div>
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">
Data
</p>
<p className="font-semibold text-gray-900">
{new Date(
selectedEvent.date + "T00:00:00"
).toLocaleDateString("pt-BR")}
</p>
</div>
<div className="bg-gray-50 p-4 rounded border border-gray-200">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-1">
Horário
</p>
<p className="font-semibold text-gray-900">
{selectedEvent.time}
</p>
</div>
</div>
{/* FOT Information Table */}
<section className="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div className="bg-gradient-to-r from-brand-purple to-brand-purple/90 px-4 py-3">
<h3 className="text-base font-bold text-white">
Informações FOT
</h3>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<tbody className="divide-y divide-gray-200">
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50 w-1/3">
FOT
</td>
<td className="px-4 py-3 text-sm text-gray-900 font-medium">
{(selectedEvent as any).fotId || '-'}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Data
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{new Date(selectedEvent.date + "T00:00:00").toLocaleDateString("pt-BR")}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Curso
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).curso || '-'}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Instituição
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).instituicao || '-'}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Ano Formatura
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).anoFormatura || '-'}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Empresa
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).empresa || '-'}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Tipo Evento
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.type}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Observações
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).observacoes || '-'}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Local
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.address.street}, {selectedEvent.address.number}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Endereço
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.address.city} - {selectedEvent.address.state}
{selectedEvent.address.zip && ` | CEP: ${selectedEvent.address.zip}`}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Horário
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{selectedEvent.time}
</td>
</tr>
<tr className="hover:bg-gray-50">
<td className="px-4 py-3 text-xs font-semibold text-gray-600 uppercase tracking-wider bg-gray-50">
Qtd Formandos
</td>
<td className="px-4 py-3 text-sm text-gray-900">
{(selectedEvent as any).qtdFormandos || '-'}
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Institution Information */}
{selectedEvent.institutionId &&
(() => {
const institution = getInstitutionById(
selectedEvent.institutionId
);
if (institution) {
return (
<section className="bg-gradient-to-br from-brand-gold/10 to-transparent border border-brand-gold/30 rounded-sm p-6">
<div className="flex items-start space-x-4">
<div className="bg-brand-gold/20 p-3 rounded-full">
<Building2
className="text-brand-gold"
size={24}
/>
</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-brand-black mb-1">
{institution.name}
</h3>
<p className="text-sm text-brand-gold uppercase tracking-wide font-medium mb-3">
{institution.type}
</p>
{/* Course Information */}
{selectedEvent.courseId &&
(() => {
const course =
getActiveCoursesByInstitutionId(
selectedEvent.institutionId
).find(
(c) => c.id === selectedEvent.courseId
);
if (course) {
return (
<div className="bg-brand-gold/10 border border-brand-gold/30 rounded px-3 py-2 mb-3">
<p className="text-xs text-gray-500 uppercase tracking-wide mb-0.5">
Curso/Turma
</p>
<p className="text-sm font-semibold text-brand-black">
{course.name} -{" "}
{course.graduationType} (
{course.year}/{course.semester}º
sem)
</p>
</div>
);
}
return null;
})()}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<div>
<p className="text-gray-500 text-xs uppercase tracking-wide">
Contato
</p>
<p className="text-gray-700 font-medium">
{institution.phone}
</p>
<p className="text-gray-600">
{institution.email}
</p>
</div>
{institution.address && (
<div>
<p className="text-gray-500 text-xs uppercase tracking-wide">
Endereço
</p>
<p className="text-gray-700">
{institution.address.street},{" "}
{institution.address.number}
</p>
<p className="text-gray-600">
{institution.address.city} -{" "}
{institution.address.state}
</p>
</div>
)}
</div>
{institution.description && (
<p className="text-gray-600 text-sm mt-3 italic border-t border-brand-gold/20 pt-3">
{institution.description}
</p>
)}
</div>
</div>
</section>
);
}
return null;
})()}
<section>
<h3 className="text-lg font-bold border-b pb-2 mb-4 text-brand-black">
Sobre o Evento
</h3>
<p className="text-gray-600 leading-relaxed whitespace-pre-wrap">
{selectedEvent.briefing || "Sem briefing detalhado."}
</p>
</section>
{selectedEvent.contacts.length > 0 && (
<section>
<h3 className="text-lg font-bold border-b pb-2 mb-4 text-brand-black">
Contatos & Responsáveis
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{selectedEvent.contacts.map((c, i) => (
<div
key={i}
className="bg-gray-50 p-4 rounded-sm border border-gray-100"
>
<p className="font-bold text-sm">{c.name}</p>
<p className="text-xs text-brand-gold uppercase tracking-wide">
{c.role}
</p>
<p className="text-sm text-gray-500 mt-1">
{c.phone}
</p>
</div>
))}
</div>
</section>
)}
</div>
<div className="lg:col-span-1 space-y-4">
{/* Localização Card */}
<div className="border p-5 rounded bg-gray-50">
<h4 className="font-bold text-sm mb-3 text-gray-700 flex items-center gap-2">
<MapPin size={16} className="text-brand-gold" />
Localização
</h4>
<p className="font-medium text-base mb-1">
{selectedEvent.address.street},{" "}
{selectedEvent.address.number}
</p>
<p className="text-gray-600 text-sm">
{selectedEvent.address.city} -{" "}
{selectedEvent.address.state}
</p>
{selectedEvent.address.zip && (
<p className="text-gray-500 text-xs mt-1">
CEP: {selectedEvent.address.zip}
</p>
)}
</div>
{/* Equipe Designada */}
{(selectedEvent.photographerIds.length > 0 ||
user.role === UserRole.BUSINESS_OWNER ||
user.role === UserRole.SUPERADMIN) && (
<div className="border p-5 rounded bg-white">
<div className="flex justify-between items-center mb-3">
<h4 className="font-bold text-sm text-gray-700 flex items-center gap-2">
<Users size={16} className="text-brand-gold" />
Equipe ({selectedEvent.photographerIds.length})
</h4>
{(user.role === UserRole.BUSINESS_OWNER ||
user.role === UserRole.SUPERADMIN) && (
<button
onClick={handleManageTeam}
className="text-brand-gold hover:text-brand-black transition-colors"
title="Adicionar fotógrafo"
>
<PlusCircle size={18} />
</button>
)}
</div>
{selectedEvent.photographerIds.length > 0 ? (
<div className="space-y-2">
{selectedEvent.photographerIds.map((id) => {
const photographer = MOCK_PHOTOGRAPHERS.find(
(p) => p.id === id
);
return (
<div
key={id}
className="flex items-center gap-2 text-sm"
>
<div
className="w-8 h-8 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
style={{
backgroundImage: `url(${
photographer?.avatar ||
`https://i.pravatar.cc/100?u=${id}`
})`,
backgroundSize: "cover",
}}
></div>
<span className="text-gray-700">
{photographer?.name || id}
</span>
</div>
);
})}
</div>
) : (
<p className="text-sm text-gray-400 italic">
Nenhum profissional atribuído
</p>
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
)}
{/* Modal de Gerenciamento de Equipe */}
{isTeamModalOpen && selectedEvent && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-2xl shadow-2xl max-w-6xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="bg-gradient-to-r from-[#492E61] to-[#5a3a7a] p-6 flex justify-between items-center">
<div>
<h2 className="text-2xl font-bold text-white mb-1">
Gerenciar Equipe
</h2>
<p className="text-white/80 text-sm">
{selectedEvent.name} -{" "}
{new Date(
selectedEvent.date + "T00:00:00"
).toLocaleDateString("pt-BR")}
</p>
</div>
<button
onClick={() => setIsTeamModalOpen(false)}
className="text-white hover:bg-white/20 rounded-full p-2 transition-colors"
>
<X size={24} />
</button>
</div>
{/* Body */}
<div className="flex-1 overflow-auto p-6">
<div className="mb-4">
<p className="text-sm text-gray-600">
Profissionais disponíveis para a data{" "}
<strong>
{new Date(
selectedEvent.date + "T00:00:00"
).toLocaleDateString("pt-BR")}
</strong>
. Clique em "Adicionar" para atribuir ao evento.
</p>
</div>
{/* Tabela de Profissionais */}
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-50 border-b-2 border-gray-200">
<th className="text-left p-4 font-semibold text-gray-700">
Profissional
</th>
<th className="text-center p-4 font-semibold text-gray-700">
Função
</th>
<th className="text-center p-4 font-semibold text-gray-700">
E-mail
</th>
<th className="text-center p-4 font-semibold text-gray-700">
Status
</th>
<th className="text-center p-4 font-semibold text-gray-700">
Ação
</th>
</tr>
</thead>
<tbody>
{MOCK_PHOTOGRAPHERS.filter((photographer) => {
const isAssigned =
selectedEvent.photographerIds.includes(
photographer.id
);
const isAvailable =
photographer.availability[selectedEvent.date] ??
false;
return isAvailable || isAssigned;
}).map((photographer) => {
const isAssigned =
selectedEvent.photographerIds.includes(
photographer.id
);
const isAvailable =
photographer.availability[selectedEvent.date] ??
false;
return (
<tr
key={photographer.id}
className="border-b border-gray-100 hover:bg-gray-50 transition-colors"
>
{/* Profissional */}
<td className="p-4">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full border-2 border-gray-200 bg-gray-300 flex-shrink-0"
style={{
backgroundImage: `url(${photographer.avatar})`,
backgroundSize: "cover",
}}
/>
<div>
<p className="font-semibold text-gray-900">
{photographer.name}
</p>
<p className="text-xs text-gray-500">
ID: {photographer.id}
</p>
</div>
</div>
</td>
{/* Função */}
<td className="p-4 text-center">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
{photographer.role}
</span>
</td>
{/* E-mail */}
<td className="p-4 text-center text-sm text-gray-600">
{photographer.email}
</td>
{/* Status */}
<td className="p-4 text-center">
{isAssigned ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-brand-gold/20 text-brand-black rounded-full text-xs font-medium">
<CheckCircle size={14} />
Atribuído
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 bg-green-100 text-green-800 rounded-full text-xs font-medium">
<UserCheck size={14} />
Disponível
</span>
)}
</td>
{/* Ação */}
<td className="p-4 text-center">
<button
onClick={() =>
togglePhotographer(photographer.id)
}
disabled={!isAvailable && !isAssigned}
className={`px-4 py-2 rounded-lg font-medium text-sm transition-colors ${
isAssigned
? "bg-red-100 text-red-700 hover:bg-red-200"
: isAvailable
? "bg-brand-gold text-white hover:bg-[#a5bd2e]"
: "bg-gray-100 text-gray-400 cursor-not-allowed"
}`}
>
{isAssigned ? "Remover" : "Adicionar"}
</button>
</td>
</tr>
);
})}
{MOCK_PHOTOGRAPHERS.filter((p) => {
const isAssigned =
selectedEvent.photographerIds.includes(p.id);
const isAvailable =
p.availability[selectedEvent.date] ?? false;
return isAvailable || isAssigned;
}).length === 0 && (
<tr>
<td colSpan={5} className="p-8 text-center">
<div className="flex flex-col items-center gap-3">
<UserX size={48} className="text-gray-300" />
<p className="text-gray-500 font-medium">
Nenhum profissional disponível para esta data
</p>
<p className="text-sm text-gray-400">
Tente selecionar outra data ou entre em contato
com a equipe
</p>
</div>
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
{/* Footer */}
<div className="border-t border-gray-200 p-6 bg-gray-50 flex justify-end gap-3">
<Button
variant="outline"
onClick={() => setIsTeamModalOpen(false)}
>
Fechar
</Button>
</div>
</div>
</div>
)}
</div>
</div>
);
};