Implement missing API endpoints
This commit is contained in:
73
src/components/shared/SearchBar.tsx
Normal file
73
src/components/shared/SearchBar.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={cn('relative', className)}>
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
className="pl-10 pr-20"
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-1/2 -translate-y-1/2 flex items-center gap-2">
|
||||||
|
{loading && <Loader2 className="w-4 h-4 animate-spin text-muted-foreground" />}
|
||||||
|
{query && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6"
|
||||||
|
onClick={handleClear}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
61
src/hooks/useAnalytics.ts
Normal file
61
src/hooks/useAnalytics.ts
Normal file
@@ -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<AnalyticsOverview | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(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,
|
||||||
|
};
|
||||||
|
};
|
||||||
81
src/hooks/useSearch.ts
Normal file
81
src/hooks/useSearch.ts
Normal file
@@ -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<any[]>([]);
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -267,6 +267,12 @@ export const adminApi = {
|
|||||||
// Places of Interest
|
// Places of Interest
|
||||||
getAllPlaces: (page = 1, limit = 10) =>
|
getAllPlaces: (page = 1, limit = 10) =>
|
||||||
apiClient.get(`/tourism/places?page=${page}&limit=${limit}`),
|
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),
|
createPlace: (data: any) => apiClient.post('/tourism/places', data),
|
||||||
updatePlace: (id: string, data: any) => apiClient.patch(`/tourism/places/${id}`, data),
|
updatePlace: (id: string, data: any) => apiClient.patch(`/tourism/places/${id}`, data),
|
||||||
deletePlace: (id: string) => apiClient.delete(`/tourism/places/${id}`),
|
deletePlace: (id: string) => apiClient.delete(`/tourism/places/${id}`),
|
||||||
@@ -304,6 +310,13 @@ export const adminApi = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
getEstablishmentById: (id: string) => apiClient.get(`/commerce/establishments/${id}`),
|
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),
|
updateEstablishment: (id: string, data: any) => apiClient.patch(`/commerce/establishments/${id}`, data),
|
||||||
deleteEstablishment: (id: string) => apiClient.delete(`/commerce/establishments/${id}`),
|
deleteEstablishment: (id: string) => apiClient.delete(`/commerce/establishments/${id}`),
|
||||||
|
|
||||||
@@ -364,6 +377,17 @@ export const adminApi = {
|
|||||||
apiClient.get(`/payments/payment-methods/${customerId}`),
|
apiClient.get(`/payments/payment-methods/${customerId}`),
|
||||||
removePaymentMethod: (paymentMethodId: string) =>
|
removePaymentMethod: (paymentMethodId: string) =>
|
||||||
apiClient.delete(`/payments/payment-methods/${paymentMethodId}`),
|
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
|
// REVIEWS MANAGEMENT
|
||||||
|
|||||||
Reference in New Issue
Block a user