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