From 797792469081553cf4315a14ec047653305490f0 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 11 Oct 2025 15:20:47 +0000 Subject: [PATCH] Implement missing API endpoints --- src/components/shared/SearchBar.tsx | 73 ++++++++++++++++++++++++++ src/hooks/useAnalytics.ts | 61 ++++++++++++++++++++++ src/hooks/useSearch.ts | 81 +++++++++++++++++++++++++++++ src/services/adminApi.ts | 24 +++++++++ 4 files changed, 239 insertions(+) create mode 100644 src/components/shared/SearchBar.tsx create mode 100644 src/hooks/useAnalytics.ts create mode 100644 src/hooks/useSearch.ts diff --git a/src/components/shared/SearchBar.tsx b/src/components/shared/SearchBar.tsx new file mode 100644 index 0000000..2d08f08 --- /dev/null +++ b/src/components/shared/SearchBar.tsx @@ -0,0 +1,73 @@ +import { useState, useEffect } from 'react'; +import { Search, X, Loader2 } from 'lucide-react'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +interface SearchBarProps { + onSearch: (query: string) => void; + placeholder?: string; + className?: string; + loading?: boolean; + debounceMs?: number; +} + +export const SearchBar = ({ + onSearch, + placeholder = 'Buscar...', + className, + loading = false, + debounceMs = 500, +}: SearchBarProps) => { + const [query, setQuery] = useState(''); + const [debouncedQuery, setDebouncedQuery] = useState(''); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedQuery(query); + }, debounceMs); + + return () => { + clearTimeout(handler); + }; + }, [query, debounceMs]); + + useEffect(() => { + onSearch(debouncedQuery); + }, [debouncedQuery, onSearch]); + + const handleClear = () => { + setQuery(''); + setDebouncedQuery(''); + onSearch(''); + }; + + return ( +
+
+ + setQuery(e.target.value)} + className="pl-10 pr-20" + /> +
+ {loading && } + {query && ( + + )} +
+
+
+ ); +}; diff --git a/src/hooks/useAnalytics.ts b/src/hooks/useAnalytics.ts new file mode 100644 index 0000000..2bf638d --- /dev/null +++ b/src/hooks/useAnalytics.ts @@ -0,0 +1,61 @@ +import { useState, useEffect } from 'react'; +import { adminApi } from '@/services/adminApi'; +import { useToast } from '@/hooks/use-toast'; + +interface AnalyticsOverview { + totalUsers: number; + totalRevenue: number; + totalBookings: number; + activeEstablishments: number; + topDestinations: Array<{ + id: string; + name: string; + visits: number; + }>; + revenueByCategory: Array<{ + category: string; + revenue: number; + }>; + userGrowth: Array<{ + date: string; + users: number; + }>; + conversionRate: number; +} + +export const useAnalytics = (period: string = 'month') => { + const [analytics, setAnalytics] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { toast } = useToast(); + + const loadAnalytics = async () => { + setLoading(true); + setError(null); + try { + const data = await adminApi.getAnalyticsOverview(period); + setAnalytics(data as AnalyticsOverview); + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Error al cargar analytics'; + setError(errorMessage); + toast({ + title: 'Error', + description: errorMessage, + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadAnalytics(); + }, [period]); + + return { + analytics, + loading, + error, + refetch: loadAnalytics, + }; +}; diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts new file mode 100644 index 0000000..dbf0dd2 --- /dev/null +++ b/src/hooks/useSearch.ts @@ -0,0 +1,81 @@ +import { useState, useCallback } from 'react'; +import { adminApi } from '@/services/adminApi'; +import { useToast } from '@/hooks/use-toast'; + +interface SearchFilters { + category?: string; + type?: string; + minRating?: number; + verified?: boolean; +} + +export const useSearch = () => { + const [searching, setSearching] = useState(false); + const [results, setResults] = useState([]); + const { toast } = useToast(); + + const searchPlaces = useCallback(async (query: string, filters?: SearchFilters) => { + if (!query.trim()) { + setResults([]); + return; + } + + setSearching(true); + try { + const response = await adminApi.searchPlaces(query, { + category: filters?.category, + minRating: filters?.minRating, + }); + setResults(response as any); + } catch (error) { + console.error('Error searching places:', error); + toast({ + title: 'Error de búsqueda', + description: error instanceof Error ? error.message : 'No se pudo realizar la búsqueda', + variant: 'destructive', + }); + setResults([]); + } finally { + setSearching(false); + } + }, [toast]); + + const searchEstablishments = useCallback(async (query: string, filters?: SearchFilters) => { + if (!query.trim()) { + setResults([]); + return; + } + + setSearching(true); + try { + const response = await adminApi.searchEstablishments(query, { + type: filters?.type, + minRating: filters?.minRating, + verified: filters?.verified, + }); + setResults(response as any); + } catch (error) { + console.error('Error searching establishments:', error); + toast({ + title: 'Error de búsqueda', + description: error instanceof Error ? error.message : 'No se pudo realizar la búsqueda', + variant: 'destructive', + }); + setResults([]); + } finally { + setSearching(false); + } + }, [toast]); + + const clearResults = useCallback(() => { + setResults([]); + }, []); + + return { + searching, + results, + searchPlaces, + searchEstablishments, + clearResults, + }; +}; diff --git a/src/services/adminApi.ts b/src/services/adminApi.ts index 740ded1..097da72 100644 --- a/src/services/adminApi.ts +++ b/src/services/adminApi.ts @@ -267,6 +267,12 @@ export const adminApi = { // Places of Interest getAllPlaces: (page = 1, limit = 10) => apiClient.get(`/tourism/places?page=${page}&limit=${limit}`), + searchPlaces: (query: string, filters?: { category?: string; minRating?: number }) => { + const params = new URLSearchParams({ query }); + if (filters?.category) params.append('category', filters.category); + if (filters?.minRating) params.append('minRating', filters.minRating.toString()); + return apiClient.get(`/tourism/places/search?${params}`); + }, createPlace: (data: any) => apiClient.post('/tourism/places', data), updatePlace: (id: string, data: any) => apiClient.patch(`/tourism/places/${id}`, data), deletePlace: (id: string) => apiClient.delete(`/tourism/places/${id}`), @@ -304,6 +310,13 @@ export const adminApi = { } }, getEstablishmentById: (id: string) => apiClient.get(`/commerce/establishments/${id}`), + searchEstablishments: (query: string, filters?: { type?: string; minRating?: number; verified?: boolean }) => { + const params = new URLSearchParams({ query }); + if (filters?.type) params.append('type', filters.type); + if (filters?.minRating) params.append('minRating', filters.minRating.toString()); + if (filters?.verified !== undefined) params.append('verified', filters.verified.toString()); + return apiClient.get(`/commerce/establishments/search?${params}`); + }, updateEstablishment: (id: string, data: any) => apiClient.patch(`/commerce/establishments/${id}`, data), deleteEstablishment: (id: string) => apiClient.delete(`/commerce/establishments/${id}`), @@ -364,6 +377,17 @@ export const adminApi = { apiClient.get(`/payments/payment-methods/${customerId}`), removePaymentMethod: (paymentMethodId: string) => apiClient.delete(`/payments/payment-methods/${paymentMethodId}`), + handleStripeWebhook: (event: any) => + apiClient.post('/payments/webhook', event), + + // ============================================================================= + // ANALYTICS MANAGEMENT + // ============================================================================= + + getAnalyticsOverview: (period?: string) => { + const params = period ? `?period=${period}` : ''; + return apiClient.get(`/analytics/overview${params}`); + }, // ============================================================================= // REVIEWS MANAGEMENT