1039 lines
39 KiB
TypeScript
1039 lines
39 KiB
TypeScript
import React, { useState, useEffect } 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 } 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
|
|
}
|
|
);
|
|
|
|
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();
|
|
}, []);
|
|
|
|
// Fetch FOTs filtered by user company
|
|
const [availableFots, setAvailableFots] = useState<any[]>([]);
|
|
const [loadingFots, setLoadingFots] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const loadFots = async () => {
|
|
// Allow FOT loading for Business Owners, Event Owners (Clients), and Superadmins
|
|
if (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.EVENT_OWNER || user?.role === UserRole.SUPERADMIN) {
|
|
// If user is not superadmin (admin generally has no empresaId but sees all or selects one, here we assume superadmin logic is separate or allowed)
|
|
// Check if regular user has empresaId
|
|
if (user?.role !== UserRole.SUPERADMIN && !user?.empresaId) {
|
|
// If no company linked, do not load FOTs
|
|
return;
|
|
}
|
|
|
|
setLoadingFots(true);
|
|
const token = localStorage.getItem("token") || "";
|
|
// Use empresaId from user context if available
|
|
const empresaId = user.empresaId;
|
|
const response = await getCadastroFot(token, empresaId);
|
|
|
|
if (response.data) {
|
|
// If we didn't filter by API (e.g. no empresaId), filter client side as fallback
|
|
const myFots = (empresaId || user.companyName)
|
|
? response.data.filter(f =>
|
|
(empresaId && f.empresa_id === empresaId) ||
|
|
(user.companyName && f.empresa_nome === user.companyName)
|
|
)
|
|
: response.data;
|
|
|
|
setAvailableFots(myFots);
|
|
}
|
|
setLoadingFots(false);
|
|
}
|
|
};
|
|
loadFots();
|
|
}, [user]);
|
|
|
|
// Derived state for dropdowns
|
|
const [selectedCourseName, setSelectedCourseName] = useState("");
|
|
const [selectedInstitutionName, setSelectedInstitutionName] = useState("");
|
|
|
|
// Populate form with initialData
|
|
useEffect(() => {
|
|
if (initialData) {
|
|
// 1. Populate standard form fields
|
|
setFormData(prev => ({
|
|
...prev,
|
|
...initialData,
|
|
startTime: initialData.time || "00:00",
|
|
locationName: (initialData as any).local_evento || (initialData as any).address?.mapLink?.includes('http') ? "" : (initialData as any).address?.mapLink || "", // Try to recover location name or clear if it's a link
|
|
fotId: initialData.fotId || "",
|
|
}));
|
|
|
|
// 2. Populate derived dropdowns if data exists
|
|
if (initialData.curso) {
|
|
setSelectedCourseName(initialData.curso);
|
|
}
|
|
if (initialData.instituicao) {
|
|
setSelectedInstitutionName(initialData.instituicao);
|
|
}
|
|
|
|
// 3. Populate address if available
|
|
if (initialData.address) {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
address: {
|
|
...prev.address,
|
|
...initialData.address,
|
|
mapLink: initialData.address.mapLink || ""
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
}, [initialData]);
|
|
|
|
// Unique Courses
|
|
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 (user?.role === UserRole.BUSINESS_OWNER || user?.role === UserRole.EVENT_OWNER) {
|
|
if (!formData.fotId) {
|
|
alert("Por favor, selecione a Turma (Cadastro FOT) antes de continuar.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
setShowToast(true);
|
|
|
|
// Prepare Payload for Agenda API
|
|
const payload = {
|
|
fot_id: formData.fotId,
|
|
tipo_evento_id: formData.typeId || "00000000-0000-0000-0000-000000000000",
|
|
data_evento: new Date(formData.date).toISOString(),
|
|
horario: formData.startTime || "",
|
|
observacoes_evento: 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,
|
|
|
|
// Default integer values
|
|
qtd_fotografos: 0,
|
|
qtd_recepcionistas: 0,
|
|
qtd_cinegrafistas: 0,
|
|
qtd_estudios: 0,
|
|
qtd_ponto_foto: 0,
|
|
qtd_ponto_id: 0,
|
|
qtd_ponto_decorado: 0,
|
|
qtd_pontos_led: 0,
|
|
qtd_plataforma_360: 0,
|
|
|
|
status_profissionais: "PENDING",
|
|
foto_faltante: 0,
|
|
recep_faltante: 0,
|
|
cine_faltante: 0,
|
|
logistica_observacoes: "",
|
|
pre_venda: true
|
|
};
|
|
|
|
// Submit to parent handler
|
|
setTimeout(() => {
|
|
if (onSubmit) {
|
|
onSubmit(formData);
|
|
}
|
|
alert("Solicitação enviada com sucesso!");
|
|
}, 1000);
|
|
} catch (e: any) {
|
|
console.error(e);
|
|
alert("Erro inesperado: " + e.message);
|
|
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 });
|
|
}
|
|
}}
|
|
type="text"
|
|
inputMode="numeric"
|
|
/>
|
|
|
|
{/* 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 ? (
|
|
<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>
|
|
) : (
|
|
<>
|
|
{/* 1. Curso */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm text-gray-600 mb-1">Curso</label>
|
|
<select
|
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
|
|
value={selectedCourseName}
|
|
onChange={e => {
|
|
setSelectedCourseName(e.target.value);
|
|
setSelectedInstitutionName("");
|
|
setFormData({ ...formData, fotId: "" });
|
|
}}
|
|
disabled={loadingFots}
|
|
>
|
|
<option value="">Selecione o Curso</option>
|
|
{uniqueCourses.map(c => <option key={c} value={c}>{c}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
{/* 2. Instituição */}
|
|
<div className="mb-4">
|
|
<label className="block text-sm text-gray-600 mb-1">Instituição</label>
|
|
<select
|
|
className="w-full px-3 py-2 border border-gray-300 rounded focus:ring-brand-gold focus:border-brand-gold"
|
|
value={selectedInstitutionName}
|
|
onChange={e => {
|
|
setSelectedInstitutionName(e.target.value);
|
|
setFormData({ ...formData, fotId: "" });
|
|
}}
|
|
disabled={!selectedCourseName}
|
|
>
|
|
<option value="">Selecione a Instituição</option>
|
|
{filteredInstitutions.map(i => <option key={i} value={i}>{i}</option>)}
|
|
</select>
|
|
</div>
|
|
|
|
{/* 3. Ano/Turma (Final FOT Selection) */}
|
|
<div className="mb-0">
|
|
<label className="block text-sm text-gray-600 mb-1">Ano/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 => setFormData({ ...formData, fotId: e.target.value })}
|
|
disabled={!selectedInstitutionName}
|
|
>
|
|
<option value="">Selecione a Turma</option>
|
|
{filteredYears.map(f => (
|
|
<option key={f.id} value={f.id}>{f.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row justify-end gap-3 sm:gap-0 mt-8">
|
|
<Button
|
|
onClick={() => setActiveTab("location")}
|
|
className="w-full sm:w-auto"
|
|
disabled={(!user?.empresaId && user?.role !== UserRole.SUPERADMIN)}
|
|
>
|
|
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>📍 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 >
|
|
);
|
|
};
|