diff --git a/frontend/components/EventCard.tsx b/frontend/components/EventCard.tsx index 60265b8..f868906 100644 --- a/frontend/components/EventCard.tsx +++ b/frontend/components/EventCard.tsx @@ -48,7 +48,7 @@ export const EventCard: React.FC = ({ event, onClick }) => {
- {new Date(event.date).toLocaleDateString()} às {event.time} + {new Date(event.date).toLocaleDateString()} às {event.startTime || event.time}{event.endTime || event.horario_fim ? ` - ${event.endTime || event.horario_fim}` : ''}
{/* Location with Tooltip */} diff --git a/frontend/components/EventForm.tsx b/frontend/components/EventForm.tsx index a6e6606..8c56987 100644 --- a/frontend/components/EventForm.tsx +++ b/frontend/components/EventForm.tsx @@ -16,11 +16,8 @@ import { AlertCircle, AlertTriangle, } from "lucide-react"; -import { - searchMapboxLocation, - MapboxResult, - reverseGeocode, -} from "../services/mapboxService"; +import { MapboxResult } from "../services/mapboxService"; +import { searchGoogleLocation, reverseGeocodeGoogle } from "../services/googleMapsService"; import { useAuth } from "../contexts/AuthContext"; import { useData } from "../contexts/DataContext"; import { UserRole } from "../types"; @@ -197,8 +194,8 @@ export const EventForm: React.FC = ({ setFormData(prev => ({ ...prev, ...initialData, - startTime: initialData.time || (initialData as any).horario || "00:00", - endTime: (initialData as any).horario_termino || prev.endTime || "", + startTime: initialData.time || initialData.startTime || (initialData as any).horario || "00:00", + endTime: initialData.endTime || (initialData as any).horario_fim || 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") @@ -293,7 +290,7 @@ export const EventForm: React.FC = ({ const timer = setTimeout(async () => { if (addressQuery.length > 3) { setIsSearching(true); - const results = await searchMapboxLocation(addressQuery); + const results = await searchGoogleLocation(addressQuery); setAddressResults(results); setIsSearching(false); } else { @@ -306,6 +303,7 @@ export const EventForm: React.FC = ({ const handleAddressSelect = (addr: MapboxResult) => { setFormData((prev: any) => ({ ...prev, + locationName: addr.placeName || prev.locationName, address: { street: addr.street, number: addr.number, @@ -322,11 +320,12 @@ export const EventForm: React.FC = ({ }; const handleMapLocationChange = async (lat: number, lng: number) => { - const addressData = await reverseGeocode(lat, lng); + const addressData = await reverseGeocodeGoogle(lat, lng); if (addressData) { setFormData((prev: any) => ({ ...prev, + locationName: addressData.placeName || prev.locationName, address: { street: addressData.street, number: addressData.number, @@ -358,11 +357,12 @@ export const EventForm: React.FC = ({ setIsGeocoding(true); try { - const results = await searchMapboxLocation(query); + const results = await searchGoogleLocation(query); if (results.length > 0) { const firstResult = results[0]; setFormData((prev: any) => ({ ...prev, + locationName: firstResult.placeName || prev.locationName, address: { ...prev.address, lat: firstResult.lat, @@ -930,7 +930,7 @@ export const EventForm: React.FC = ({
void; defaultTime?: string; + defaultEndTime?: string; } const timeSlots = [ @@ -19,7 +20,7 @@ const timeSlots = [ "19:00", "20:00", "21:00", "22:00", "23:00", "00:00" ]; -const EventScheduler: React.FC = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime }) => { +const EventScheduler: React.FC = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime, defaultEndTime }) => { const { token, user } = useAuth(); const { professionals, events, functions } = useData(); const [escalas, setEscalas] = useState([]); @@ -29,7 +30,7 @@ const EventScheduler: React.FC = ({ agendaId, dataEvento, a // New entry state const [selectedProf, setSelectedProf] = useState(""); const [startTime, setStartTime] = useState(defaultTime || "08:00"); - const [endTime, setEndTime] = useState("12:00"); // Could calculated based on start, but keep simple + const [endTime, setEndTime] = useState(defaultEndTime || "12:00"); // Could calculated based on start, but keep simple const [role, setRole] = useState(""); const isEditable = user?.role === UserRole.SUPERADMIN || user?.role === UserRole.BUSINESS_OWNER; diff --git a/frontend/contexts/DataContext.tsx b/frontend/contexts/DataContext.tsx index afe584c..fe75014 100644 --- a/frontend/contexts/DataContext.tsx +++ b/frontend/contexts/DataContext.tsx @@ -680,6 +680,8 @@ export const DataProvider: React.FC<{ children: ReactNode }> = ({ name: e.observacoes_evento || e.tipo_evento_nome || "Evento sem nome", // Fallback mapping date: e.data_evento ? e.data_evento.split('T')[0] : "", time: e.horario || "00:00", + startTime: e.horario || "00:00", + endTime: e.horario_fim || "", type: (e.tipo_evento_nome || "Outro") as EventType, // Map string to enum if possible, or keep string status: mapStatus(e.status), // Map from backend status with fallback address: { diff --git a/frontend/pages/EventDetails.tsx b/frontend/pages/EventDetails.tsx index 2a8166e..3c40c5d 100644 --- a/frontend/pages/EventDetails.tsx +++ b/frontend/pages/EventDetails.tsx @@ -95,7 +95,7 @@ const EventDetails: React.FC = () => {

Horário

-

{event.horario || event.time || "Não definido"}

+

{event.startTime || event.horario || event.time || "Não definido"}{event.endTime || event.horario_fim ? ` - ${event.endTime || event.horario_fim}` : ''}

@@ -114,7 +114,8 @@ const EventDetails: React.FC = () => { dataEvento={event.date} allowedProfessionals={event.assignments} onUpdateStats={setCalculatedStats} - defaultTime={event.time} + defaultTime={event.startTime || event.time} + defaultEndTime={event.endTime || event.horario_fim} /> {/* Right: Logistics (Carros) - Only visible if user has permission */} diff --git a/frontend/services/googleMapsService.ts b/frontend/services/googleMapsService.ts new file mode 100644 index 0000000..5d24522 --- /dev/null +++ b/frontend/services/googleMapsService.ts @@ -0,0 +1,130 @@ +import { MapboxResult } from "./mapboxService"; + +// Exporting MapboxResult from mapboxService to keep compatibility +// but using Google Maps to fetch the data. + +const GOOGLE_MAPS_KEY = import.meta.env.VITE_GOOGLE_MAPS_KEY || ""; + +/** + * Busca endereços e locais usando a API de Geocoding do Google + */ +export async function searchGoogleLocation(query: string, country: string = "br"): Promise { + if (!GOOGLE_MAPS_KEY || GOOGLE_MAPS_KEY.includes("YOUR")) { + console.warn("⚠️ Google Maps Token não configurado em .env.local"); + return []; + } + + try { + const encodedQuery = encodeURIComponent(query); + const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodedQuery}&components=country:${country}&language=pt-BR&key=${GOOGLE_MAPS_KEY}`; + + const response = await fetch(url); + const data = await response.json(); + + if (data.status !== "OK" || !data.results) { + if (data.status !== "ZERO_RESULTS") { + console.error("Google Maps API Error:", data.status, data.error_message); + } + return []; + } + + return data.results.map((result: any) => { + // Find components + const getComponent = (type: string) => { + const comp = result.address_components.find((c: any) => c.types.includes(type)); + return comp ? comp.long_name : ""; + }; + const getShortComponent = (type: string) => { + const comp = result.address_components.find((c: any) => c.types.includes(type)); + return comp ? comp.short_name : ""; + }; + + const street = getComponent("route"); + const number = getComponent("street_number"); + const city = getComponent("administrative_area_level_2") || getComponent("locality"); + const state = getShortComponent("administrative_area_level_1"); + const zip = getComponent("postal_code"); + + // Verify if it's a POI by checking the types of the location + const isPoi = result.types.includes("establishment") || result.types.includes("stadium") || result.types.includes("point_of_interest"); + + let placeName = undefined; + let finalStreet = street; + + if (isPoi) { + // Obter o nome do estabelecimento do formatted_address (ex: "Allianz Parque, Av. Francisco Matarazzo...") + placeName = result.address_components.find((c: any) => c.types.includes("establishment") || c.types.includes("point_of_interest"))?.long_name; + if (!placeName && result.formatted_address) { + placeName = result.formatted_address.split(",")[0]; + } + } + + // Se a query não conseguiu resolver route/street, usa a formatação + if (!finalStreet) finalStreet = result.formatted_address; + + return { + placeName, + description: placeName ? `${placeName} - ${result.formatted_address}` : result.formatted_address, + street: finalStreet, + number, + city, + state, + zip, + lat: result.geometry.location.lat, + lng: result.geometry.location.lng, + mapLink: `https://www.google.com/maps/search/?api=1&query=${result.geometry.location.lat},${result.geometry.location.lng}`, + }; + }); + } catch (error) { + console.error("Erro ao buscar no Google Maps:", error); + return []; + } +} + +/** + * Busca reversa via Google Maps Geocoding + */ +export async function reverseGeocodeGoogle(lat: number, lng: number): Promise { + if (!GOOGLE_MAPS_KEY) return null; + + try { + const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lng}&language=pt-BR&key=${GOOGLE_MAPS_KEY}`; + const response = await fetch(url); + const data = await response.json(); + + if (data.status === "OK" && data.results.length > 0) { + const result = data.results[0]; + + const getComponent = (type: string) => { + const comp = result.address_components.find((c: any) => c.types.includes(type)); + return comp ? comp.long_name : ""; + }; + const getShortComponent = (type: string) => { + const comp = result.address_components.find((c: any) => c.types.includes(type)); + return comp ? comp.short_name : ""; + }; + + const street = getComponent("route") || result.formatted_address; + const number = getComponent("street_number"); + const city = getComponent("administrative_area_level_2") || getComponent("locality"); + const state = getShortComponent("administrative_area_level_1"); + const zip = getComponent("postal_code"); + + return { + description: result.formatted_address, + street, + number, + city, + state, + zip, + lat, + lng, + mapLink: `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`, + }; + } + return null; + } catch (error) { + console.error("Erro no reverse geocode do Google:", error); + return null; + } +} diff --git a/frontend/services/mapboxService.ts b/frontend/services/mapboxService.ts index 58bf39a..d0b42d9 100644 --- a/frontend/services/mapboxService.ts +++ b/frontend/services/mapboxService.ts @@ -16,9 +16,11 @@ export interface MapboxFeature { place_type: string[]; text: string; address?: string; + properties?: any; } export interface MapboxResult { + placeName?: string; description: string; street: string; number: string; @@ -56,17 +58,11 @@ export async function searchMapboxLocation( `access_token=${MAPBOX_TOKEN}&` + `country=${country}&` + `language=pt&` + + `types=poi,address&` + `limit=10`; - // Add proximity bias based on region - const region = localStorage.getItem("photum_selected_region"); - if (region === "MG") { - // Belo Horizonteish center - url += `&proximity=-43.9378,-19.9208`; - } else { - // São Pauloish center (Default) - url += `&proximity=-46.6333,-23.5505`; - } + // Removed proximity bias to prevent Mapbox from hiding national POIs (like Estádio Pacaembu) + // when the user's region is set far away from the POI. console.log("🔍 Buscando endereço:", query); const response = await fetch(url); @@ -94,9 +90,18 @@ export async function searchMapboxLocation( const addressMatch = feature.address || feature.text.match(/\d+/)?.[0] || ""; + let placeName = undefined; + let street = feature.text; + + if (feature.place_type.includes("poi")) { + placeName = feature.text; + street = feature.properties?.address || feature.place_name.split(",")[0]; + } + return { + placeName, description: feature.place_name, - street: feature.text, + street, number: addressMatch, city: place?.text || "", state: region?.short_code?.replace("BR-", "") || region?.text || "",