feat(agenda): migração busca POI para Google Maps e correção do horário de término
Frontend: - Migração da API de Geocoding do Mapbox para o Google Maps Places API (googleMapsService.ts) no formulário de eventos, garantindo a busca correta pelo nome de locais (estádios, teatros) e com autopreenchimento. - Correção do fluxo de estado do 'horario_fim', propagando e persistindo o 'endTime' pelo DataContext, garantindo a população dos dados na edição do EventForm. - Adição da visualização do horário final na listagem do Dashboard, no EventCard, painéis de EventDetails e atualização das props defaultEndTime no EventScheduler. Backend: - Atualização e migração dos arquivos gerados pelo sqlc (models.go, agenda.sql.go) para suportar operações no novo design do banco. - Atualização síncrona dos artefatos Swagger de documentação de API (docs.go, swagger.json, swagger.yaml).
This commit is contained in:
parent
d2c37d7b2c
commit
7a06d4e691
7 changed files with 165 additions and 26 deletions
|
|
@ -48,7 +48,7 @@ export const EventCard: React.FC<EventCardProps> = ({ event, onClick }) => {
|
|||
<div className="space-y-2 sm:space-y-3">
|
||||
<div className="flex items-center text-gray-500 text-xs sm:text-sm">
|
||||
<Calendar size={14} className="sm:w-4 sm:h-4 mr-1.5 sm:mr-2 text-brand-gold flex-shrink-0" />
|
||||
<span className="truncate">{new Date(event.date).toLocaleDateString()} às {event.time}</span>
|
||||
<span className="truncate">{new Date(event.date).toLocaleDateString()} às {event.startTime || event.time}{event.endTime || event.horario_fim ? ` - ${event.endTime || event.horario_fim}` : ''}</span>
|
||||
</div>
|
||||
|
||||
{/* Location with Tooltip */}
|
||||
|
|
|
|||
|
|
@ -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<EventFormProps> = ({
|
|||
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<EventFormProps> = ({
|
|||
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<EventFormProps> = ({
|
|||
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<EventFormProps> = ({
|
|||
};
|
||||
|
||||
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<EventFormProps> = ({
|
|||
|
||||
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<EventFormProps> = ({
|
|||
|
||||
<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)
|
||||
BUSCA DE ENDEREÇO (POWERED BY GOOGLE)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ interface EventSchedulerProps {
|
|||
allowedProfessionals?: { professional_id?: string; professionalId?: string; status?: string }[] | string[]; // IDs or Objects
|
||||
onUpdateStats?: (stats: { studios: number }) => 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<EventSchedulerProps> = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime }) => {
|
||||
const EventScheduler: React.FC<EventSchedulerProps> = ({ agendaId, dataEvento, allowedProfessionals, onUpdateStats, defaultTime, defaultEndTime }) => {
|
||||
const { token, user } = useAuth();
|
||||
const { professionals, events, functions } = useData();
|
||||
const [escalas, setEscalas] = useState<any[]>([]);
|
||||
|
|
@ -29,7 +30,7 @@ const EventScheduler: React.FC<EventSchedulerProps> = ({ 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;
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ const EventDetails: React.FC = () => {
|
|||
<Clock className="w-5 h-5 text-brand-purple mt-1" />
|
||||
<div>
|
||||
<p className="text-xs text-gray-500 uppercase font-bold">Horário</p>
|
||||
<p className="font-medium text-gray-800">{event.horario || event.time || "Não definido"}</p>
|
||||
<p className="font-medium text-gray-800">{event.startTime || event.horario || event.time || "Não definido"}{event.endTime || event.horario_fim ? ` - ${event.endTime || event.horario_fim}` : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
|
|
@ -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 */}
|
||||
|
|
|
|||
130
frontend/services/googleMapsService.ts
Normal file
130
frontend/services/googleMapsService.ts
Normal file
|
|
@ -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<MapboxResult[]> {
|
||||
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<MapboxResult | null> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 || "",
|
||||
|
|
|
|||
Loading…
Reference in a new issue