Implement missing API endpoints

This commit is contained in:
gpt-engineer-app[bot]
2025-10-11 15:20:47 +00:00
parent c732180bce
commit 7977924690
4 changed files with 239 additions and 0 deletions

View 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
View 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
View 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,
};
};

View File

@@ -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