Implement remaining API endpoints
This commit is contained in:
27
src/App.tsx
27
src/App.tsx
@@ -40,6 +40,9 @@ import Security from "./pages/dashboard/Security";
|
||||
import VehicleManagement from "./pages/dashboard/VehicleManagement";
|
||||
import Sustainability from "./pages/dashboard/Sustainability";
|
||||
import Establishments from "./pages/dashboard/Establishments";
|
||||
import Analytics from "./pages/dashboard/Analytics";
|
||||
import SearchPlaces from "./pages/dashboard/SearchPlaces";
|
||||
import SearchEstablishments from "./pages/dashboard/SearchEstablishments";
|
||||
// Commerce pages (for retail stores)
|
||||
import CommerceStore from "./pages/dashboard/commerce/Store";
|
||||
import CommercePOS from "./pages/dashboard/commerce/POSTerminal";
|
||||
@@ -302,6 +305,30 @@ const AppRouter = () => (
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/dashboard/analytics" element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<Analytics />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/dashboard/search-places" element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<SearchPlaces />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/dashboard/search-establishments" element={
|
||||
<ProtectedRoute>
|
||||
<DashboardLayout>
|
||||
<SearchEstablishments />
|
||||
</DashboardLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Commerce Routes */}
|
||||
<Route path="/dashboard/commerce/store" element={
|
||||
<ProtectedRoute>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Camera, Send, X, Star } from 'lucide-react';
|
||||
import { ReviewPhotoUpload } from './ReviewPhotoUpload';
|
||||
import { ImageUploader } from '@/components/shared/ImageUploader';
|
||||
|
||||
interface Review {
|
||||
id: string;
|
||||
@@ -151,10 +151,21 @@ export function ReviewReplyDialog({ review, onReply, onClose }: ReviewReplyDialo
|
||||
|
||||
{/* Photo Upload Dialog */}
|
||||
{showPhotoUpload && (
|
||||
<ReviewPhotoUpload
|
||||
onUpload={handleImageUpload}
|
||||
onClose={() => setShowPhotoUpload(false)}
|
||||
/>
|
||||
<Dialog open={true} onOpenChange={() => setShowPhotoUpload(false)}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Subir Imágenes</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ImageUploader
|
||||
onUploadComplete={(urls) => {
|
||||
setImages([...images, ...urls]);
|
||||
setShowPhotoUpload(false);
|
||||
}}
|
||||
maxFiles={5}
|
||||
category="review-reply"
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
249
src/pages/dashboard/Analytics.tsx
Normal file
249
src/pages/dashboard/Analytics.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useState } from 'react';
|
||||
import DashboardLayout from '@/components/DashboardLayout';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { useAnalytics } from '@/hooks/useAnalytics';
|
||||
import { TrendingUp, Users, DollarSign, MapPin, Loader2 } from 'lucide-react';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const Analytics = () => {
|
||||
const [period, setPeriod] = useState('month');
|
||||
const { analytics, loading, error, refetch } = useAnalytics(period);
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('es-DO', {
|
||||
style: 'currency',
|
||||
currency: 'DOP',
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const StatCard = ({
|
||||
title,
|
||||
value,
|
||||
icon: Icon,
|
||||
description
|
||||
}: {
|
||||
title: string;
|
||||
value: string | number;
|
||||
icon: any;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">{title}</CardTitle>
|
||||
<Icon className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{value}</div>
|
||||
{description && (
|
||||
<p className="text-xs text-muted-foreground mt-1">{description}</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Analytics</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Métricas y estadísticas de tu plataforma
|
||||
</p>
|
||||
</div>
|
||||
<Select value={period} onValueChange={setPeriod}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="day">Hoy</SelectItem>
|
||||
<SelectItem value="week">Esta Semana</SelectItem>
|
||||
<SelectItem value="month">Este Mes</SelectItem>
|
||||
<SelectItem value="year">Este Año</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Loading State */}
|
||||
{loading && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-8 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-40" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-64 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && (
|
||||
<Card className="border-destructive">
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center space-y-4">
|
||||
<p className="text-destructive">{error}</p>
|
||||
<button
|
||||
onClick={refetch}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
||||
>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Analytics Data */}
|
||||
{analytics && !loading && (
|
||||
<>
|
||||
{/* Stats Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard
|
||||
title="Total Usuarios"
|
||||
value={analytics.totalUsers.toLocaleString()}
|
||||
icon={Users}
|
||||
description="Usuarios registrados"
|
||||
/>
|
||||
<StatCard
|
||||
title="Ingresos Totales"
|
||||
value={formatCurrency(analytics.totalRevenue)}
|
||||
icon={DollarSign}
|
||||
description="Ingresos del período"
|
||||
/>
|
||||
<StatCard
|
||||
title="Reservaciones"
|
||||
value={analytics.totalBookings.toLocaleString()}
|
||||
icon={TrendingUp}
|
||||
description="Reservaciones completadas"
|
||||
/>
|
||||
<StatCard
|
||||
title="Establecimientos"
|
||||
value={analytics.activeEstablishments.toLocaleString()}
|
||||
icon={MapPin}
|
||||
description="Establecimientos activos"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Top Destinations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Destinos Más Visitados</CardTitle>
|
||||
<CardDescription>
|
||||
Top destinos del período seleccionado
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{analytics.topDestinations.map((destination, index) => (
|
||||
<div key={destination.id} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-sm font-bold text-primary">
|
||||
{index + 1}
|
||||
</div>
|
||||
<span className="font-medium">{destination.name}</span>
|
||||
</div>
|
||||
<span className="text-muted-foreground">
|
||||
{destination.visits.toLocaleString()} visitas
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Revenue by Category */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ingresos por Categoría</CardTitle>
|
||||
<CardDescription>
|
||||
Distribución de ingresos por tipo de servicio
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{analytics.revenueByCategory.map((item) => (
|
||||
<div key={item.category}>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium capitalize">{item.category}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{formatCurrency(item.revenue)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-secondary h-2 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="bg-primary h-full rounded-full transition-all"
|
||||
style={{
|
||||
width: `${(item.revenue / analytics.totalRevenue) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* User Growth Chart */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Crecimiento de Usuarios</CardTitle>
|
||||
<CardDescription>
|
||||
Evolución de usuarios registrados - Tasa de conversión: {analytics.conversionRate.toFixed(2)}%
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="h-64 flex items-end justify-between gap-2">
|
||||
{analytics.userGrowth.map((point) => (
|
||||
<div key={point.date} className="flex-1 flex flex-col items-center gap-2">
|
||||
<div
|
||||
className="w-full bg-primary rounded-t transition-all hover:bg-primary/80"
|
||||
style={{
|
||||
height: `${(point.users / Math.max(...analytics.userGrowth.map(p => p.users))) * 100}%`,
|
||||
minHeight: '20px',
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(point.date).toLocaleDateString('es', { day: 'numeric', month: 'short' })}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Analytics;
|
||||
212
src/pages/dashboard/SearchEstablishments.tsx
Normal file
212
src/pages/dashboard/SearchEstablishments.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
import { useState } from 'react';
|
||||
import DashboardLayout from '@/components/DashboardLayout';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { SearchBar } from '@/components/shared/SearchBar';
|
||||
import { useSearch } from '@/hooks/useSearch';
|
||||
import { Building2, Star, Loader2, MapPin, CheckCircle } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
const SearchEstablishments = () => {
|
||||
const [type, setType] = useState<string>('all');
|
||||
const [minRating, setMinRating] = useState<number>(0);
|
||||
const [verifiedOnly, setVerifiedOnly] = useState(false);
|
||||
const { searching, results, searchEstablishments } = useSearch();
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
if (query.trim()) {
|
||||
searchEstablishments(query, {
|
||||
type: type !== 'all' ? type : undefined,
|
||||
minRating: minRating > 0 ? minRating : undefined,
|
||||
verified: verifiedOnly,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
return Array.from({ length: 5 }, (_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={14}
|
||||
className={i < rating ? "text-yellow-400 fill-yellow-400" : "text-muted-foreground"}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
restaurant: 'Restaurante',
|
||||
hotel: 'Hotel',
|
||||
shop: 'Tienda',
|
||||
attraction: 'Atracción',
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Buscar Establecimientos</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Encuentra hoteles, restaurantes, tiendas y más
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Búsqueda</CardTitle>
|
||||
<CardDescription>
|
||||
Usa filtros para refinar tu búsqueda
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<SearchBar
|
||||
onSearch={handleSearch}
|
||||
placeholder="Buscar establecimientos..."
|
||||
loading={searching}
|
||||
/>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Tipo</label>
|
||||
<Select value={type} onValueChange={setType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todos</SelectItem>
|
||||
<SelectItem value="restaurant">Restaurantes</SelectItem>
|
||||
<SelectItem value="hotel">Hoteles</SelectItem>
|
||||
<SelectItem value="shop">Tiendas</SelectItem>
|
||||
<SelectItem value="attraction">Atracciones</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Calificación Mínima</label>
|
||||
<Select value={minRating.toString()} onValueChange={(v) => setMinRating(Number(v))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">Todas</SelectItem>
|
||||
<SelectItem value="3">3+ ⭐</SelectItem>
|
||||
<SelectItem value="4">4+ ⭐</SelectItem>
|
||||
<SelectItem value="4.5">4.5+ ⭐</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Filtros</label>
|
||||
<div className="flex items-center space-x-2 h-10">
|
||||
<Switch
|
||||
id="verified"
|
||||
checked={verifiedOnly}
|
||||
onCheckedChange={setVerifiedOnly}
|
||||
/>
|
||||
<Label htmlFor="verified" className="cursor-pointer">
|
||||
Solo verificados
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results */}
|
||||
{searching && (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!searching && results.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-12">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<Building2 className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No hay resultados para mostrar</p>
|
||||
<p className="text-sm mt-2">Intenta realizar una búsqueda</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!searching && results.length > 0 && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{results.map((establishment: any) => (
|
||||
<Card key={establishment.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<CardTitle className="text-lg">{establishment.name}</CardTitle>
|
||||
{establishment.verified && (
|
||||
<CheckCircle className="w-4 h-4 text-blue-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{renderStars(Math.floor(establishment.rating))}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{establishment.rating.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="secondary" className="capitalize">
|
||||
{getTypeLabel(establishment.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{establishment.description}
|
||||
</p>
|
||||
|
||||
{establishment.location && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<MapPin className="w-4 h-4 mt-0.5 text-muted-foreground flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{establishment.location.address}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{establishment.owner && (
|
||||
<div className="text-sm">
|
||||
<span className="text-muted-foreground">Propietario: </span>
|
||||
<span className="font-medium">{establishment.owner.name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<Badge
|
||||
variant={
|
||||
establishment.status === 'active' ? 'default' :
|
||||
establishment.status === 'pending' ? 'secondary' :
|
||||
'destructive'
|
||||
}
|
||||
>
|
||||
{establishment.status === 'active' ? 'Activo' :
|
||||
establishment.status === 'pending' ? 'Pendiente' :
|
||||
'Suspendido'}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(establishment.createdAt).toLocaleDateString('es')}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchEstablishments;
|
||||
165
src/pages/dashboard/SearchPlaces.tsx
Normal file
165
src/pages/dashboard/SearchPlaces.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useState } from 'react';
|
||||
import DashboardLayout from '@/components/DashboardLayout';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { SearchBar } from '@/components/shared/SearchBar';
|
||||
import { useSearch } from '@/hooks/useSearch';
|
||||
import { MapPin, Star, Loader2 } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
const SearchPlaces = () => {
|
||||
const [category, setCategory] = useState<string>('all');
|
||||
const [minRating, setMinRating] = useState<number>(0);
|
||||
const { searching, results, searchPlaces } = useSearch();
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
if (query.trim()) {
|
||||
searchPlaces(query, {
|
||||
category: category !== 'all' ? category : undefined,
|
||||
minRating: minRating > 0 ? minRating : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
return Array.from({ length: 5 }, (_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={14}
|
||||
className={i < rating ? "text-yellow-400 fill-yellow-400" : "text-muted-foreground"}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Buscar Lugares</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Encuentra lugares de interés turístico
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Búsqueda</CardTitle>
|
||||
<CardDescription>
|
||||
Usa filtros para refinar tu búsqueda
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<SearchBar
|
||||
onSearch={handleSearch}
|
||||
placeholder="Buscar por nombre, descripción..."
|
||||
loading={searching}
|
||||
/>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium mb-2 block">Categoría</label>
|
||||
<Select value={category} onValueChange={setCategory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Todas</SelectItem>
|
||||
<SelectItem value="beach">Playas</SelectItem>
|
||||
<SelectItem value="museum">Museos</SelectItem>
|
||||
<SelectItem value="park">Parques</SelectItem>
|
||||
<SelectItem value="restaurant">Restaurantes</SelectItem>
|
||||
<SelectItem value="hotel">Hoteles</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium mb-2 block">Calificación Mínima</label>
|
||||
<Select value={minRating.toString()} onValueChange={(v) => setMinRating(Number(v))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">Todas</SelectItem>
|
||||
<SelectItem value="3">3+ ⭐</SelectItem>
|
||||
<SelectItem value="4">4+ ⭐</SelectItem>
|
||||
<SelectItem value="4.5">4.5+ ⭐</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results */}
|
||||
{searching && (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-primary" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!searching && results.length === 0 && (
|
||||
<Card>
|
||||
<CardContent className="py-12">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<MapPin className="w-12 h-12 mx-auto mb-4 opacity-50" />
|
||||
<p>No hay resultados para mostrar</p>
|
||||
<p className="text-sm mt-2">Intenta realizar una búsqueda</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!searching && results.length > 0 && (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{results.map((place: any) => (
|
||||
<Card key={place.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg">{place.name}</CardTitle>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{renderStars(Math.floor(place.rating))}
|
||||
</div>
|
||||
<span className="text-sm font-medium">{place.rating.toFixed(1)}</span>
|
||||
</div>
|
||||
</div>
|
||||
{place.category && (
|
||||
<Badge variant="secondary" className="capitalize">
|
||||
{place.category}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{place.description}
|
||||
</p>
|
||||
{place.location && (
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<MapPin className="w-4 h-4 mt-0.5 text-muted-foreground flex-shrink-0" />
|
||||
<span className="text-muted-foreground">{place.location.address}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between pt-2 border-t">
|
||||
<Badge variant={place.status === 'active' ? 'default' : 'secondary'}>
|
||||
{place.status === 'active' ? 'Activo' : 'Inactivo'}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{new Date(place.createdAt).toLocaleDateString('es')}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPlaces;
|
||||
Reference in New Issue
Block a user