photum/frontend/components/EventForm.tsx
JoaoVitorMS0 a1d5434414 feat: Implementar sistema completo de gestão de equipe e restrições
- Adicionar restrições de exclusão de FOT quando há eventos associados
- Implementar tooltips para motivos de recusa de eventos por fotógrafos
- Filtrar eventos recusados das listas de fotógrafos
- Adicionar sistema de filtros avançados no modal de gerenciar equipe
- Implementar campos completos de gestão de equipe (fotógrafos, recepcionistas, cinegrafistas, estúdios, pontos de foto, pontos decorados, pontos LED)
- Adicionar colunas de gestão na tabela principal com cálculos automáticos de profissionais faltantes
- Implementar controle de visibilidade da seção de gestão apenas para empresas
- Adicionar status visual "Profissionais OK" com indicadores de completude
- Implementar sistema de cálculo em tempo real de equipe necessária vs confirmada
- Adicionar validações condicionais baseadas no tipo de usuário
2026-01-13 13:24:38 -03:00

1251 lines
49 KiB
TypeScript

import React, { useState, useEffect, useMemo } from "react";
import { EventType, EventStatus, Address } from "../types";
import { Input, Select } from "./Input";
import { Button } from "./Button";
import {
MapPin,
Upload,
Plus,
X,
Check,
FileText,
ExternalLink,
Search,
CheckCircle,
Building2,
AlertCircle,
AlertTriangle,
} from "lucide-react";
import {
searchMapboxLocation,
MapboxResult,
reverseGeocode,
} from "../services/mapboxService";
import { useAuth } from "../contexts/AuthContext";
import { useData } from "../contexts/DataContext";
import { UserRole } from "../types";
import { InstitutionForm } from "./InstitutionForm";
import { MapboxMap } from "./MapboxMap";
import { getEventTypes, EventTypeResponse, getCadastroFot, createAgenda, getCompanies } from "../services/apiService";
interface EventFormProps {
onCancel: () => void;
onSubmit: (data: any) => void;
initialData?: any;
}
export const EventForm: React.FC<EventFormProps> = ({
onCancel,
onSubmit,
initialData,
}) => {
const { user, token: userToken } = useAuth();
const {
addInstitution,
} = useData();
const [activeTab, setActiveTab] = useState<
"details" | "location" | "briefing" | "files"
>("details");
const [addressQuery, setAddressQuery] = useState("");
const [addressResults, setAddressResults] = useState<MapboxResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [isGeocoding, setIsGeocoding] = useState(false);
const [showToast, setShowToast] = useState(false);
const [showInstitutionForm, setShowInstitutionForm] = useState(false);
const [eventTypes, setEventTypes] = useState<EventTypeResponse[]>([]);
const [isBackendDown, setIsBackendDown] = useState(false);
const [isLoadingEventTypes, setIsLoadingEventTypes] = useState(true);
// Default State or Initial Data
const [formData, setFormData] = useState(
initialData || {
name: "",
date: "",
time: "",
startTime: "",
endTime: "",
type: "",
status: EventStatus.PLANNING,
locationName: "", // New field: Nome do Local
address: {
street: "",
number: "",
city: "",
state: "",
zip: "",
lat: -22.7394,
lng: -47.3314,
mapLink: "",
} as Address,
briefing: "",
contacts: [{ name: "", role: "", phone: "" }],
files: [] as File[],
institutionId: "", // Legacy, might clear or keep for compatibility
attendees: "",
courseId: "", // Legacy
fotId: "", // New field for FOT linkage
// Novos campos de gestão de equipe
qtdFormandos: "",
qtdFotografos: "",
qtdRecepcionistas: "",
qtdCinegrafistas: "",
qtdEstudios: "",
qtdPontosFoto: "",
qtdPontosDecorados: "",
qtdPontosLed: "",
}
);
const isClientRequest = user?.role === UserRole.EVENT_OWNER;
const formTitle = initialData
? "Editar Evento"
: isClientRequest
? "Solicitar Orçamento/Evento"
: "Cadastrar Novo Evento";
const submitLabel = initialData
? "Salvar Alterações"
: isClientRequest
? "Enviar Solicitação"
: "Criar Evento";
// Fetch Event Types
useEffect(() => {
const fetchEventTypes = async () => {
setIsLoadingEventTypes(true);
const response = await getEventTypes();
if (response.isBackendDown) {
setIsBackendDown(true);
setEventTypes([]);
} else if (response.data) {
setIsBackendDown(false);
setEventTypes(response.data);
}
setIsLoadingEventTypes(false);
};
fetchEventTypes();
}, []);
// Derived state for dropdowns
const [companies, setCompanies] = useState<any[]>([]); // New state for companies list
const [selectedCompanyId, setSelectedCompanyId] = useState(""); // Changed from Name to ID
const [selectedCourseName, setSelectedCourseName] = useState("");
const [selectedInstitutionName, setSelectedInstitutionName] = useState("");
// Load Companies for Business Owner / Superadmin
useEffect(() => {
if (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) {
const fetchCompanies = async () => {
try {
const response = await getCompanies();
if (response.data) {
setCompanies(response.data);
}
} catch (error) {
console.error("Failed to load companies", error);
}
};
fetchCompanies();
}
}, [user]);
// Populate form with initialData
useEffect(() => {
if (initialData) {
console.log("EventForm received initialData:", initialData);
// Robust mapping for API snake_case fields
const mapLink = (initialData as any).local_evento || (initialData.address && initialData.address.mapLink) || "";
const mappedFotId = (initialData as any).fot_id || initialData.fotId || "";
const mappedEmpresaId = (initialData as any).empresa_id || (initialData as any).empresaId || "";
const mappedObservacoes = (initialData as any).observacoes_evento || initialData.name || "";
console.log("Mapped Values:", { mappedFotId, mappedEmpresaId, mappedObservacoes, mapLink });
// Parse Endereco String if Object is missing but String exists
// Format expected: "Rua, Numero - Cidade/UF" or similar
let addressData = initialData.address || {
street: "", number: "", city: "", state: "", zip: "", lat: -22.7394, lng: -47.3314, mapLink: ""
};
if (!initialData.address && (initialData as any).endereco) {
// Simple heuristic parser or just default. User might need to refine.
// Doing a best-effort copy to street if unstructured
addressData = { ...addressData, street: (initialData as any).endereco || "" };
}
// Safety: ensure all strings
addressData = {
street: addressData.street || "",
number: addressData.number || "",
city: addressData.city || "",
state: addressData.state || "",
zip: addressData.zip || "",
lat: addressData.lat || -22.7394,
lng: addressData.lng || -47.3314,
mapLink: addressData.mapLink || ""
};
// 1. Populate standard form fields
setFormData(prev => ({
...prev,
...initialData,
startTime: initialData.time || (initialData as any).horario || "00:00",
endTime: (initialData as any).horario_termino || prev.endTime || "",
locationName: mapLink.includes('http') ? "" : mapLink, // Avoid putting URL in name
fotId: mappedFotId,
name: mappedObservacoes, // Map Observacoes to Name field (displayed as "Observacoes do Evento")
briefing: mappedObservacoes, // Sync briefing
address: addressData,
// Mapear campos de gestão de equipe
qtdFormandos: initialData.qtdFormandos || initialData.attendees || "",
qtdFotografos: initialData.qtdFotografos || "",
qtdRecepcionistas: initialData.qtdRecepcionistas || "",
qtdCinegrafistas: initialData.qtdCinegrafistas || "",
qtdEstudios: initialData.qtdEstudios || "",
qtdPontosFoto: initialData.qtdPontosFoto || "",
qtdPontosDecorados: initialData.qtdPontosDecorados || "",
qtdPontosLed: initialData.qtdPontosLed || "",
}));
// 2. Populate derived dropdowns if data exists
if (mappedEmpresaId && (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN)) {
console.log("Setting Selected Company:", mappedEmpresaId);
setSelectedCompanyId(mappedEmpresaId);
}
if (initialData.curso || (initialData as any).curso_nome) {
setSelectedCourseName(initialData.curso || (initialData as any).curso_nome || "");
}
if (initialData.instituicao) {
setSelectedInstitutionName(initialData.instituicao || "");
}
}
}, [initialData, user?.role]);
// Fetch FOTs filtered by user company OR selected company
const [availableFots, setAvailableFots] = useState<any[]>([]);
const [loadingFots, setLoadingFots] = useState(false);
useEffect(() => {
const loadFots = async () => {
// Determine which company ID to use
let targetEmpresaId = user?.empresaId;
if (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) {
// Must select a company first
if (!selectedCompanyId) {
setAvailableFots([]);
return;
}
targetEmpresaId = selectedCompanyId;
}
// If we have a target company (or user is linked), fetch FOTs
if (targetEmpresaId || user?.role === UserRole.EVENT_OWNER) {
setLoadingFots(true);
// Clear previous FOTs to force UI update and avoid stale data
setAvailableFots([]);
console.log("Fetching FOTs for company:", targetEmpresaId);
const token = localStorage.getItem("token") || "";
const response = await getCadastroFot(token, targetEmpresaId);
if (response.data) {
console.log("FOTs loaded:", response.data.length);
setAvailableFots(response.data);
}
setLoadingFots(false);
}
};
loadFots();
}, [user, selectedCompanyId]);
// Unique Courses (from availableFots - which are already specific to the company)
const uniqueCourses = Array.from(new Set(availableFots.map(f => f.curso_nome))).sort();
// Filtered Institutions based on Course
const filteredInstitutions = availableFots
.filter(f => f.curso_nome === selectedCourseName)
.map(f => f.instituicao)
.filter((v, i, a) => a.indexOf(v) === i)
.sort();
// Filtered Years based on Course + Inst
const filteredYears = availableFots
.filter(f => f.curso_nome === selectedCourseName && f.instituicao === selectedInstitutionName)
.map(f => ({ id: f.id, label: f.ano_formatura_label }));
// Address Autocomplete Logic using Mapbox
useEffect(() => {
const timer = setTimeout(async () => {
if (addressQuery.length > 3) {
setIsSearching(true);
const results = await searchMapboxLocation(addressQuery);
setAddressResults(results);
setIsSearching(false);
} else {
setAddressResults([]);
}
}, 500);
return () => clearTimeout(timer);
}, [addressQuery]);
const handleAddressSelect = (addr: MapboxResult) => {
setFormData((prev: any) => ({
...prev,
address: {
street: addr.street,
number: addr.number,
city: addr.city,
state: addr.state,
zip: addr.zip,
lat: addr.lat,
lng: addr.lng,
mapLink: addr.mapLink,
},
}));
setAddressQuery("");
setAddressResults([]);
};
const handleMapLocationChange = async (lat: number, lng: number) => {
const addressData = await reverseGeocode(lat, lng);
if (addressData) {
setFormData((prev: any) => ({
...prev,
address: {
street: addressData.street,
number: addressData.number,
city: addressData.city,
state: addressData.state,
zip: addressData.zip,
lat: addressData.lat,
lng: addressData.lng,
mapLink: addressData.mapLink,
},
}));
} else {
setFormData((prev: any) => ({
...prev,
address: {
...prev.address,
lat,
lng,
mapLink: `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`,
},
}));
}
};
const handleManualAddressChange = async () => {
const { street, number, city, state } = formData.address;
const query = `${street} ${number}, ${city}, ${state}`.trim();
if (query.length < 5) return;
setIsGeocoding(true);
try {
const results = await searchMapboxLocation(query);
if (results.length > 0) {
const firstResult = results[0];
setFormData((prev: any) => ({
...prev,
address: {
...prev.address,
lat: firstResult.lat,
lng: firstResult.lng,
mapLink: firstResult.mapLink,
},
}));
}
} catch (error) {
console.error("Erro ao geocodificar endereço manual:", error);
} finally {
setIsGeocoding(false);
}
};
const addContact = () => {
setFormData((prev: any) => ({
...prev,
contacts: [...prev.contacts, { name: "", role: "", phone: "" }],
}));
};
const removeContact = (index: number) => {
const newContacts = [...formData.contacts];
newContacts.splice(index, 1);
setFormData((prev: any) => ({ ...prev, contacts: newContacts }));
};
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
setFormData((prev: any) => ({
...prev,
files: [...prev.files, ...Array.from(e.target.files || [])],
}));
}
};
const handleSubmit = async () => {
// Validation
if (!formData.name) return alert("Preencha o tipo de evento");
if (!formData.date) return alert("Preencha a data");
if (!formData.attendees || parseInt(formData.attendees) <= 0) return alert("Preencha o número de formandos");
// Validação condicional apenas para empresas
if ((user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN)) {
if (!formData.qtdFotografos || parseInt(formData.qtdFotografos) <= 0) {
return alert("Preencha a quantidade de fotógrafos necessários");
}
}
// Ensure typeId is valid
let finalTypeId = formData.typeId;
if (!finalTypeId || finalTypeId === "00000000-0000-0000-0000-000000000000") {
// Try to match by name
const matchingType = eventTypes.find(t => t.nome === formData.type);
if (matchingType) {
finalTypeId = matchingType.id;
} else {
// If strictly required by DB, we must stop.
// But for legacy compatibility, maybe we prompt user
return alert("Tipo de evento inválido. Por favor, selecione novamente o tipo do evento.");
}
}
if (!formData.fotId) {
alert("ERRO CRÍTICO: Turma (FOT) não identificada. Por favor, selecione a Turma ou Empresa novamente.");
return;
}
try {
setShowToast(true);
// Prepare Payload for Agenda API
const payload = {
fot_id: formData.fotId, // Must be valid UUID
tipo_evento_id: finalTypeId,
data_evento: new Date(formData.date).toISOString(),
horario: formData.startTime || "",
observacoes_evento: formData.name || formData.briefing || "",
local_evento: formData.locationName || "",
endereco: `${formData.address.street}, ${formData.address.number} - ${formData.address.city}/${formData.address.state}`,
qtd_formandos: parseInt(formData.attendees) || 0,
// Campos de gestão de equipe
qtd_fotografos: parseInt(formData.qtdFotografos) || 0,
qtd_recepcionistas: parseInt(formData.qtdRecepcionistas) || 0,
qtd_cinegrafistas: parseInt(formData.qtdCinegrafistas) || 0,
qtd_estudios: parseInt(formData.qtdEstudios) || 0,
qtd_pontos_foto: parseInt(formData.qtdPontosFoto) || 0,
qtd_pontos_decorados: parseInt(formData.qtdPontosDecorados) || 0,
qtd_pontos_led: parseInt(formData.qtdPontosLed) || 0,
status_profissionais: "PENDING",
cine_faltante: 0,
logistica_observacoes: "",
pre_venda: true
};
// Submit to parent handler
if (onSubmit) {
await onSubmit(payload);
}
alert("Solicitação enviada com sucesso!");
} catch (e: any) {
console.error(e);
alert("Erro ao salvar: " + (e.message || "Erro desconhecido"));
setShowToast(false);
}
};
const handleInstitutionSubmit = (institutionData: any) => {
const newInstitution = {
...institutionData,
id: `inst-${Date.now()}`,
ownerId: user?.id || "",
};
addInstitution(newInstitution);
setFormData((prev: any) => ({ ...prev, institutionId: newInstitution.id }));
setShowInstitutionForm(false);
};
if (showInstitutionForm) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<InstitutionForm
onCancel={() => setShowInstitutionForm(false)}
onSubmit={handleInstitutionSubmit}
userId={user?.id || ""}
/>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow-xl overflow-hidden max-w-4xl mx-auto border border-gray-100 slide-up relative">
{/* Success Toast */}
{showToast && (
<div className="absolute top-4 right-4 z-50 bg-brand-black text-white px-4 sm:px-6 py-3 sm:py-4 rounded shadow-2xl flex items-center space-x-2 sm:space-x-3 fade-in">
<CheckCircle className="text-brand-gold h-5 w-5 sm:h-6 sm:w-6" />
<div>
<h4 className="font-bold text-xs sm:text-sm">Sucesso!</h4>
<p className="text-[10px] sm:text-xs text-gray-300">
As informações foram salvas.
</p>
</div>
</div>
)}
{/* Form Header */}
<div className="bg-gray-50 border-b px-4 sm:px-6 md:px-8 py-4 sm:py-5 md:py-6">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 sm:gap-0">
<div>
<h2 className="text-xl sm:text-2xl font-serif text-brand-black">
{formTitle}
</h2>
<p className="text-xs sm:text-sm text-gray-500 mt-1">
{isClientRequest
? "Preencha os detalhes do seu sonho. Nossa equipe analisará em breve."
: "Preencha as informações técnicas do evento."}
</p>
</div>
{/* Step indicators */}
<div className="hidden sm:flex space-x-2">
{["details", "location", "briefing", "files"].map((tab, idx) => (
<div
key={tab}
className={`flex flex-col items-center ${activeTab === tab ? "opacity-100" : "opacity-40"
}`}
>
<span
className={`w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-xs font-bold mb-1 ${activeTab === tab
? "bg-[#492E61] text-white"
: "bg-gray-200 text-gray-600"
}`}
>
{idx + 1}
</span>
</div>
))}
</div>
</div>
</div>
{/* Mobile Tabs */}
<div className="lg:hidden border-b border-gray-200 bg-white overflow-x-auto scrollbar-hide">
<div className="flex min-w-max">
{[
{ id: "details", label: "Detalhes", icon: "1" },
{ id: "location", label: "Localização", icon: "2" },
{
id: "briefing",
label: isClientRequest ? "Desejos" : "Briefing",
icon: "3",
},
{ id: "files", label: "Arquivos", icon: "4" },
].map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id as any)}
className={`flex-1 px-4 py-3 text-xs sm:text-sm font-medium transition-colors border-b-2 whitespace-nowrap ${activeTab === item.id
? "text-brand-gold border-brand-gold bg-brand-gold/5"
: "text-gray-500 border-transparent hover:bg-gray-50"
}`}
>
<span className="inline-block w-5 h-5 rounded-full text-[10px] leading-5 text-center mr-1.5 ${
activeTab === item.id ? 'bg-brand-gold text-white' : 'bg-gray-200 text-gray-600'
}">
{item.icon}
</span>
{item.label}
</button>
))}
</div>
</div>
<div className="lg:grid lg:grid-cols-4 min-h-[500px]">
{/* Desktop Sidebar Navigation */}
<div className="hidden lg:block lg:col-span-1 border-r border-gray-100 bg-gray-50/50 p-4 space-y-2">
{[
{ id: "details", label: "Detalhes & Data" },
{ id: "location", label: "Localização" },
{
id: "briefing",
label: isClientRequest ? "Seus Desejos" : "Briefing & Equipe",
},
{ id: "files", label: "Inspirações" },
].map((item) => (
<button
key={item.id}
onClick={() => setActiveTab(item.id as any)}
className={`w-full text-left px-4 py-3 rounded-sm text-sm font-medium transition-colors ${activeTab === item.id
? "bg-white shadow-sm text-brand-gold border-l-4 border-brand-gold"
: "text-gray-500 hover:bg-gray-100"
}`}
>
{item.label}
</button>
))}
</div>
{/* Form Content */}
<div className="lg:col-span-3 p-4 sm:p-6 md:p-8">
{activeTab === "details" && (
<div className="space-y-6 fade-in">
<div className="grid grid-cols-1 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Evento*
</label>
<select
required
value={formData.typeId || ""}
onChange={(e) => {
const selectedId = e.target.value;
const selectedType = eventTypes.find((t) => t.id === selectedId);
setFormData({
...formData,
typeId: selectedId,
type: selectedType?.nome || ""
});
}}
disabled={isLoadingEventTypes || isBackendDown}
className="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-brand-gold disabled:bg-gray-100 disabled:cursor-not-allowed"
>
<option value="">Selecione o tipo de evento</option>
{eventTypes.map((type) => (
<option key={type.id} value={type.id}>
{type.nome}
</option>
))}
</select>
{isBackendDown && (
<div className="mt-2 flex items-center gap-2 text-sm text-red-600">
<AlertTriangle size={16} />
<span>Backend não está rodando. Não é possível carregar os tipos de eventos.</span>
</div>
)}
{isLoadingEventTypes && !isBackendDown && (
<div className="mt-2 text-sm text-gray-500">
Carregando tipos de eventos...
</div>
)}
</div>
<Input
label="Observações do Evento (Opcional)"
placeholder="Ex: Cerimônia de Colação de Grau"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
/>
<Input
label="Data Pretendida"
type="date"
value={formData.date}
onChange={(e) =>
setFormData({ ...formData, date: e.target.value })
}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="Horário de Início*"
type="time"
value={formData.startTime}
onChange={(e) =>
setFormData({ ...formData, startTime: e.target.value })
}
required
/>
<Input
label="Horário de Término (Opcional)"
type="time"
value={formData.endTime}
onChange={(e) =>
setFormData({ ...formData, endTime: e.target.value })
}
/>
</div>
<Input
label="Número de Formandos*"
placeholder="Ex: 50"
value={formData.attendees}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, attendees: value, qtdFormandos: value });
}
}}
type="text"
inputMode="numeric"
required
/>
{/* Seção de Gestão de Equipe - Apenas para Empresas */}
{(user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && (
<div className="bg-blue-50 p-4 rounded-md border border-blue-200">
<h3 className="text-sm font-medium text-blue-700 mb-4 uppercase tracking-wider">Gestão de Equipe e Recursos</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Input
label="Qtd. Fotógrafos*"
placeholder="Ex: 2"
value={formData.qtdFotografos}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdFotografos: value });
}
}}
type="text"
inputMode="numeric"
required
/>
<Input
label="Qtd. Recepcionistas"
placeholder="Ex: 1"
value={formData.qtdRecepcionistas}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdRecepcionistas: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Cinegrafistas"
placeholder="Ex: 1"
value={formData.qtdCinegrafistas}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdCinegrafistas: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Estúdios"
placeholder="Ex: 1"
value={formData.qtdEstudios}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdEstudios: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Pontos de Foto"
placeholder="Ex: 3"
value={formData.qtdPontosFoto}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdPontosFoto: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Pontos Decorados"
placeholder="Ex: 2"
value={formData.qtdPontosDecorados}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdPontosDecorados: value });
}
}}
type="text"
inputMode="numeric"
/>
<Input
label="Qtd. Pontos LED"
placeholder="Ex: 1"
value={formData.qtdPontosLed}
onChange={(e) => {
const value = e.target.value;
if (value === "" || /^\d+$/.test(value)) {
setFormData({ ...formData, qtdPontosLed: value });
}
}}
type="text"
inputMode="numeric"
/>
</div>
</div>
)}
{/* Dynamic FOT Selection */}
<div className="bg-gray-50 p-4 rounded-md border border-gray-200">
<h3 className="text-sm font-medium text-gray-700 mb-4 uppercase tracking-wider">Seleção da Turma</h3>
{!user?.empresaId && user?.role !== UserRole.SUPERADMIN && user?.role !== UserRole.BUSINESS_OWNER ? (
<div className="bg-red-50 border-l-4 border-red-400 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<AlertCircle className="h-5 w-5 text-red-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-red-700">
Sua conta não está vinculada a nenhuma empresa. Por favor, entre em contato com a administração para regularizar seu cadastro antes de solicitar um evento.
</p>
</div>
</div>
</div>
) : (
<>
{/* 0. Empresa (Only for Business Owner / Superadmin) */}
{(user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && (
<div className="mb-4">
<label className="block text-sm text-gray-600 mb-1">Empresa</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
value={selectedCompanyId}
onChange={e => {
setSelectedCompanyId(e.target.value);
setFormData({ ...formData, fotId: "" });
}}
>
<option value="">Selecione a Empresa</option>
{companies.map(c => <option key={c.id} value={c.id}>{c.nome}</option>)}
</select>
</div>
)}
{/* Warning if Company Selected but No FOTs */}
{selectedCompanyId && !loadingFots && availableFots.length === 0 && (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && (
<div className="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4">
<div className="flex">
<div className="flex-shrink-0">
<AlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
</div>
<div className="ml-3">
<p className="text-sm text-yellow-700">
Esta empresa selecionada ainda não possui turmas, cursos ou FOTs cadastrados.
</p>
</div>
</div>
</div>
)}
{/* Consolidated Turma Selection */}
<div className="mb-0">
<label className="block text-sm text-gray-600 mb-1">Selecione a Turma</label>
<select
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
value={formData.fotId || ""}
onChange={e => {
const selectedFotId = e.target.value;
const selectedFot = availableFots.find(f => f.id === selectedFotId);
if (selectedFot) {
setFormData({
...formData,
fotId: selectedFotId,
// Optional: You might want to store denormalized data if needed, but ID is usually enough
});
} else {
setFormData({ ...formData, fotId: "" });
}
}}
disabled={loadingFots || ((user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.SUPERADMIN) && !selectedCompanyId)}
>
<option value="">Selecione a Turma (Curso - Instituição - Ano)</option>
{availableFots.map(f => (
<option key={f.id} value={f.id}>
{f.curso_nome} - {f.instituicao} - {f.ano_formatura_label} (FOT {f.fot})
</option>
))}
</select>
{loadingFots && <p className="text-xs text-gray-500 mt-1">Carregando turmas...</p>}
</div>
</>
)}
</div>
</div>
<div className="flex flex-col sm:flex-row justify-between gap-3 mt-8">
<Button
variant="outline"
onClick={onCancel}
className="w-full sm:w-auto order-2 sm:order-1"
>
Voltar
</Button>
<Button
onClick={() => setActiveTab("location")}
className="w-full sm:w-auto order-1 sm:order-2"
disabled={(!user?.empresaId && user?.role !== UserRole.SUPERADMIN && user?.role !== UserRole.BUSINESS_OWNER)}
>
Próximo: Localização
</Button>
</div>
</div>
)}
{activeTab === "location" && (
<div className="space-y-6 fade-in">
{/* Nome do Local */}
<Input
label="Nome do Local"
placeholder="Ex: Espaço das Américas, Salão de Festas X"
value={formData.locationName}
onChange={(e) => setFormData({ ...formData, locationName: e.target.value })}
/>
<div className="bg-gray-50 p-4 rounded-lg border border-gray-200">
<label className="block text-sm font-medium text-gray-700 mb-2 tracking-wide uppercase text-xs">
Busca de Endereço (Powered by Mapbox)
</label>
<div className="relative">
<input
className="w-full px-4 py-2 border border-gray-300 rounded-sm focus:outline-none focus:ring-1 focus:ring-brand-gold focus:border-brand-gold transition-colors pr-10"
placeholder="Digite o nome do local ou endereço..."
value={addressQuery}
onChange={(e) => setAddressQuery(e.target.value)}
/>
<div className="absolute right-3 top-2.5 text-gray-400">
{isSearching ? (
<div className="animate-spin h-5 w-5 border-2 border-brand-gold rounded-full border-t-transparent"></div>
) : (
<Search size={20} />
)}
</div>
</div>
{addressResults.length > 0 && (
<ul className="absolute z-10 w-full bg-white border mt-1 shadow-lg rounded-sm max-h-64 overflow-y-auto">
{addressResults.map((addr, idx) => (
<li
key={idx}
className="px-4 py-3 hover:bg-gray-50 cursor-pointer text-sm border-b border-gray-50 last:border-0"
onClick={() => handleAddressSelect(addr)}
>
<div className="flex justify-between items-start">
<div className="flex items-start">
<MapPin
size={16}
className="mt-0.5 mr-2 text-brand-gold flex-shrink-0"
/>
<div>
<p className="font-medium text-gray-800">
{addr.description}
</p>
<p className="text-xs text-gray-500 mt-0.5">
{addr.city}, {addr.state}
</p>
</div>
</div>
{addr.mapLink && (
<span className="flex items-center text-[10px] text-blue-600 bg-blue-50 px-2 py-1 rounded ml-2">
<img
src="https://www.google.com/images/branding/product/ico/maps15_bnuw3a_32dp.png"
alt="Maps"
className="w-3 h-3 mr-1"
/>
Maps
</span>
)}
</div>
</li>
))}
</ul>
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="sm:col-span-2">
<Input
label="Rua"
value={formData.address.street}
onChange={(e) => {
setFormData({
...formData,
address: {
...formData.address,
street: e.target.value,
},
});
}}
onBlur={handleManualAddressChange}
placeholder="Digite o nome da rua"
/>
</div>
<Input
label="Número"
placeholder="123"
value={formData.address.number}
onChange={(e) => {
const value = e.target.value;
setFormData({
...formData,
address: { ...formData.address, number: value },
});
}}
onBlur={handleManualAddressChange}
type="text"
inputMode="numeric"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<Input
label="Cidade"
value={formData.address.city}
onChange={(e) => {
setFormData({
...formData,
address: { ...formData.address, city: e.target.value },
});
}}
onBlur={handleManualAddressChange}
placeholder="Digite a cidade"
/>
<Input
label="Estado"
value={formData.address.state}
onChange={(e) => {
const value = e.target.value.toUpperCase().slice(0, 2);
setFormData({
...formData,
address: { ...formData.address, state: value },
});
}}
onBlur={handleManualAddressChange}
placeholder="SP"
maxLength={2}
/>
</div>
{/* Mapa Interativo */}
<div className="mt-6">
<label className="block text-sm font-medium text-gray-700 mb-3 tracking-wide uppercase text-xs flex items-center justify-between">
<span className="flex items-center gap-2"><MapPin className="w-4 h-4 text-[#B9CF32]" /> Mapa Interativo - Ajuste a Localização Exata</span>
{isGeocoding && (
<span className="text-xs text-brand-gold flex items-center normal-case">
<div className="animate-spin h-3 w-3 border-2 border-brand-gold rounded-full border-t-transparent mr-2"></div>
Localizando no mapa...
</span>
)}
</label>
<MapboxMap
initialLat={formData.address.lat || -22.7394}
initialLng={formData.address.lng || -47.3314}
onLocationChange={handleMapLocationChange}
height="300px"
className="sm:h-[400px] md:h-[450px]"
/>
</div>
{formData.address.mapLink && (
<div className="bg-gray-50 p-3 rounded border border-gray-200 flex items-center justify-between">
<span className="text-xs text-gray-500 flex items-center">
<Check size={14} className="mr-1 text-green-500" />
Localização verificada via Mapbox
</span>
<a
href={formData.address.mapLink}
target="_blank"
rel="noreferrer"
className="text-xs text-brand-gold flex items-center hover:underline"
>
Ver no mapa <ExternalLink size={12} className="ml-1" />
</a>
</div>
)}
<div className="flex flex-col sm:flex-row justify-between gap-3 mt-8">
<Button
variant="outline"
onClick={() => setActiveTab("details")}
className="w-full sm:w-auto"
>
Voltar
</Button>
<Button
onClick={() => setActiveTab("briefing")}
className="w-full sm:w-auto"
>
Próximo
</Button>
</div>
</div>
)}
{activeTab === "briefing" && (
<div className="space-y-6 fade-in">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1 tracking-wide uppercase text-xs">
{isClientRequest
? "Conte-nos sobre o seu sonho"
: "Briefing Técnico"}
</label>
<textarea
className="w-full border border-gray-300 rounded-sm p-3 focus:outline-none focus:border-brand-gold h-32 text-sm"
placeholder={
isClientRequest
? "Qual o estilo do casamento? Quais fotos são indispensáveis? Fale um pouco sobre vocês..."
: "Instruções técnicas..."
}
value={formData.briefing}
onChange={(e) =>
setFormData({ ...formData, briefing: e.target.value })
}
></textarea>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-gray-700 tracking-wide uppercase text-xs">
Contatos / Responsáveis
</label>
<button
onClick={addContact}
className="text-xs text-brand-gold font-bold hover:underline flex items-center"
>
<Plus size={14} className="mr-1" /> Adicionar
</button>
</div>
<div className="space-y-3">
{formData.contacts.map((contact: any, idx: number) => (
<div key={idx} className="flex space-x-2 items-start">
<Input
label={idx === 0 ? "Nome" : ""}
placeholder="Nome"
value={contact.name}
onChange={(e) => {
const newContacts = [...formData.contacts];
newContacts[idx].name = e.target.value;
setFormData({ ...formData, contacts: newContacts });
}}
/>
<Input
label={idx === 0 ? "Papel" : ""}
placeholder="Ex: Cerimonialista"
value={contact.role}
onChange={(e) => {
const newContacts = [...formData.contacts];
newContacts[idx].role = e.target.value;
setFormData({ ...formData, contacts: newContacts });
}}
/>
<button
onClick={() => removeContact(idx)}
className={`mt-1 p-2 text-gray-400 hover:text-red-500 ${idx === 0 ? "mt-7" : ""
}`}
>
<X size={16} />
</button>
</div>
))}
</div>
</div>
<div className="flex flex-col sm:flex-row justify-between gap-3 mt-8">
<Button
variant="outline"
onClick={() => setActiveTab("location")}
className="w-full sm:w-auto"
>
Voltar
</Button>
<Button
onClick={() => setActiveTab("files")}
className="w-full sm:w-auto"
>
Próximo
</Button>
</div>
</div>
)}
{activeTab === "files" && (
<div className="space-y-6 fade-in">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-10 text-center hover:bg-gray-50 transition-colors relative">
<input
type="file"
multiple
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleFileUpload}
/>
<Upload size={40} className="mx-auto text-gray-400 mb-4" />
<p className="text-sm text-gray-600 font-medium">
{isClientRequest
? "Anexe referências visuais (Moodboard)"
: "Anexe contratos e cronogramas"}
</p>
<p className="text-xs text-gray-400 mt-1">
PDF, JPG, PNG (Max 10MB)
</p>
</div>
{(formData.files || []).length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">
Arquivos Selecionados:
</h4>
{formData.files.map((file: any, idx: number) => (
<div
key={idx}
className="flex items-center justify-between p-3 bg-gray-50 rounded border border-gray-100"
>
<div className="flex items-center">
<FileText size={18} className="text-brand-gold mr-3" />
<div>
<p className="text-sm font-medium">{file.name}</p>
<p className="text-xs text-gray-400">
{(file.size / 1024).toFixed(1)} KB
</p>
</div>
</div>
<Check size={16} className="text-green-500" />
</div>
))}
</div>
)}
<div className="flex flex-col sm:flex-row justify-between gap-3 mt-8">
<Button
variant="outline"
onClick={() => setActiveTab("briefing")}
className="w-full sm:w-auto"
>
Voltar
</Button>
<Button
onClick={handleSubmit}
variant="secondary"
className="w-full sm:w-auto"
>
{submitLabel}
</Button>
</div>
</div>
)}
</div>
</div >
</div >
);
};